OpenAPI(原名Swagger)是目前比较流行的定义HTTP API的协议。但是OpenAPI的定义文件是方便机器处理的格式,不易编写和阅读。这里介绍一种使用go-swagger,根据Go代码生成OpenAPI定义文件的方法。该方法只使用Go代码来定义API,不强求Server或者Client也使用Go。

目前go-swagger只能生成OpenAPI 2.0格式的定义。这个也是现在广泛使用的格式。go-swagger未来会支持OpenAPI 3.0。

本文假设已经熟悉Go语法,只对go-swagger的扩展部分进行详细解释。

假设要为一个宠物商店创建一套API。这套API支持定义宠物(Pet)。本文展示如何使用Go来定义这个API。

虽然go-swagger不强求使用Go工程,不过为了便于利用补全和代码高亮,这里还是推荐为API单独创建一个Go工程。

首先是定义API的基础信息,比如这套API的名字,协议,支持的序列化格式,基础路径,以及一些安全信息。这些信息定义在petstore.go文件里。

// Package petstore The API of the pet store.
//
// The pet store is a demo service to show how to use go-swagger generate
// OpenAPI (Swagger) Spec.
//
// Version: 1.0.0
// BasePath: /v1/petstore
// Schemes: https
// Consumes:
// - application/json
// Produces:
// - application/json
//
// swagger:meta
package petstore

//go:generate go run github.com/go-swagger/go-swagger/cmd/swagger@latest generate spec -o openapi.yaml
//go:generate go run github.com/go-swagger/go-swagger/cmd/swagger@latest validate openapi.yaml

根据Go的规范,包的说明,要以Package petstore开头。该行的剩余部分,也就是The API of the pet store.会作为OpenAPI的title部分,其余非go-swagger定义的注释,会作为OpenAPI的description

注意:如果包说明里不包含description的部分,go-swagger会把第一行原本作为title的部分作为description,并把title留空。这样定义的OpenAPI文件不符合规范。所以请把两段描述写完整。

Version:行开始,是对go-swagger的定义。这部分定义了生成的OpenAPI文件其他基础内容。具体可用的选项可以参照swagger:meta。最后一行的swagger:meta表示这段注释用于go-swagger生成基础信息。

之后的两行go:generate用来生成OpenAPI文件。为了保证执行时不会缺少可执行文件,这里使用go run github.com/go-swagger/go-swagger/cmd/swagger@latest来调用go-swagger。如果可以保证执行环境已经安装了go-swagger,这两行go run ...可以直接简化为swagger。简化后会提高生成时的执行速度。

安装go-swagger可以参照官网。如果有Go环境,可以直接通过go install github.com/go-swagger/go-swagger/cmd/swagger@latest安装。注意需要把$GOPATH/bin加入到$PATH里,方便执行。

第一行generate spec -o openapi.yaml会根据当前目录的内容,生成OpenAPI文件。第二行validate openapi.yaml会验证这个OpenAPI文件是否合规。

有了这个文件,就可以通过执行go generate .来生成OpenAPI文件。执行后会生成:

$ cat openapi.yaml 
basePath: /v1/petstore
consumes:
- application/json
info:
  description: |-
    The pet store is a demo service to show how to use go-swagger generate
    OpenAPI (Swagger) Spec.
  title: The API of the pet store.
  version: 1.0.0
paths: {}
produces:
- application/json
schemes:
- https
swagger: "2.0"

我们还没有定义任何API,所以paths字段为空。后面会逐步补充这个字段的内容。

之后在pet.go文件里定义与Pet实例相关的API。

首先定义Pet结构:

// NewPet provides data to create a pet.
//
// swagger:model
type NewPet struct {
        // The name of the pet.
        //
        // min length: 1
        // max length: 64
        // example: Yuki
        Name string `json:"name"`
        // The contact email for the pet.
        //
        // min length: 1
        // max length: 64
        // example: person@domain.com
        // swagger:strfmt email
        Contact string `json:"contact"`
}

// Pet provides data of a pet.
//
// swagger:model
type Pet struct {
        // The pet id.
        //
        // example: 1
        ID uint64 `json:"id"`
        // The name of the pet.
        //
        // example: Yuki
        Name string `json:"name"`
        // The contact email for the pet.
        //
        // example: person@domain.com
        // swagger: strfmt email
        Contact string `json:"contact"`
}

其中NewPet用于创建和修改Pet数据,Pet用于展示Pet数据。这两个结构有一些细微差别,比如NewPet不会具有ID信息,另外Pet不需要定义输入限制。在简单场景下,可以合并为一个结构,或者将NewPet匿名嵌入Pet,简化结构。

swagger:model会被go-swagger处理为预定义好的结构,并将定义放入definitions里。这个结构会在多个API操作方法间复用。

go-swagger会根据Go结构的类型,生成对应的类型。每一个字段的注释,可以指定更详细的值约束,比如例子中的min length: 1max length: 64,就约定了对应字段字符串的长度应该在[1, 64]之间。还可以使用swagger:strfmt对字符串值做进一步的语义约束。

为了在处理出错可以返回另外的结构,还需要定义错误结构:

type Error struct {
        ID     string `json:"error_id"`
        Detail string `json:"error_detail"`
}

简单的结构可以不用写明所有的信息,甚至不用标记swagger:modelgo-swagger会自动根据之后方法的定义,来查找对应的结构定义。

处理的结构定义好后,就可以定义具体的操作方法。为了覆盖最广泛的情况,这里以更新Pet的方法作为讲解例子。

// swagger:route POST /pets/{pet_id} pet UpdatePet
//
// Update data of a pet.
//
// responses:
//   200: PetReply
//   default: ErrorReply

swagger:route用来定义一个具体的API方法,格式是swagger:route <method> <url> <tag> ... <operation id>。其中tag可以有0个或者任意个,给这个方法加上若干个标签。不管有多少tag,最后一个参数operation id是这个方法的名字。在一个API定义文件里,这个方法名字需要具有唯一性。

具体到例子里,swagger:router POST /pets/{pet_id} pet CreatePet的含义如下:

  • 遵循常用的API原则,将更新方法定义在POST /pets/{pet_id}端点。
    • 路径有一个输入参数pet_id
  • 给这个方法加入pet标签。
  • 方法名字为CreatePet

之后是方法的说明信息。

responses是定义go-swagger的返回信息,可以根据不同的HTTP Code定义不同的结构。例子里在操作正常也就是Code 200时返回PetReply结构,其他情况下返回ErrorReply结构。

先放下输出结构,看一下如何定义输入结构。

// swagger:parameters UpdatePet RetrievePet DeletePet
type PetIDArg struct {
        // in: path
        ID uint64 `json:"pet_id"`
}

// swagger:parameters CreatePet UpdatePet
type NewPetArg struct {
        // in: body
        NewPet *NewPet
}

swagger:parameters用来定义个输入结构,格式是swagger:parameters <operation id> <operation id> ...。其中operation id是用到这个输入结构的方法名字。需要注意的是,可以有多个输入结构用在一个方法上,比如例子里的两个结构都引用了UpdatePet。因为一个方法的输入,不仅有HTTP Post报文,还有URL里面的信息,甚至可能会从Header里拿信息。为了能复用这些结构,go-swagger可以将不同部分的输入,定义到不同的结构里,并引用到同一个方法上。

例子里,PetIDArg通过in: path定义了路径参数pet_id,其中pet_id是用过json:"pet_id"定义的。另一个结构NewPetArg通过in: body定义了Post报文结构NewPet,其中NewPet是上文描述过的一个结构。

之后是输出结构。

// PetReply returns a pet.
//
// swagger:response
type PetReply struct {
        // in: body
        Pet *Pet
}

// ErrorReply replys an error of API calling.
//
// swagger:response
type ErrorReply struct {
        // in: body
        Error Error
}

swagger:response用来定义一个输出结构。其名字与swagger:route定义的responses相同。in: body表示对应的字段用于返回的HTTP报文。

至此,Pet的更新方法定义完成。也解释了go-swagger绝大部分内容。可以直接执行go generate .或者swagger generate spec查看输出。关于使用go-swagger生成规范文件的更多细节,可以查阅官网

作为例子,下面的pet.go文件给出了Pet操作的所有定义。配合之前提到的petstore.go文件,可以直接用来生成petstore的OpenAPI定义。

package petstore

// NewPet provides data to create a pet.
//
// swagger:model
type NewPet struct {
	// The name of the pet.
	//
	// min length: 1
	// max length: 64
	// example: Yuki
	Name string `json:"name"`
	// The contact email for the pet.
	//
	// min length: 1
	// max length: 64
	// example: person@domain.com
	// swagger:strfmt email
	Contact string `json:"contact"`
}

// Pet provides data of a pet.
//
// swagger:model
type Pet struct {
	// The pet id.
	//
	// example: 1
	ID uint64 `json:"id"`
	// The name of the pet.
	//
	// example: Yuki
	Name string `json:"name"`
	// The contact email for the pet.
	//
	// example: person@domain.com
	// swagger: strfmt email
	Contact string `json:"contact"`
}

// swagger:parameters UpdatePet RetrievePet DeletePet
type PetIDArg struct {
	// in: path
	ID uint64 `json:"pet_id"`
}

// swagger:parameters CreatePet UpdatePet
type NewPetArg struct {
	// in: body
	NewPet *NewPet
}

// PetReply returns a pet.
//
// swagger:response
type PetReply struct {
	// in: body
	Pet *Pet
}

// PetsReply returns a list of pets.
//
// swagger:response
type PetsReply struct {
	// in: body
	Pets []*Pet
}

type Error struct {
	ID     string `json:"error_id"`
	Detail string `json:"error_detail"`
}

// ErrorReply replys an error of API calling.
//
// swagger:response
type ErrorReply struct {
	// in: body
	Error Error
}

type ErrorInput struct {
	Error
	Fields map[string]Error `json:"error_fields"`
}

// ErrorInputReply replys an error with details of input data.
//
// swagger:response
type ErrorInputReply struct {
	// in: body
	ErrorInput ErrorInput
}

// swagger:route GET /pets pet ListPet
//
// Get the list of pets.
//
// responses:
//   200: PetsReply
//   default: ErrorReply

// swagger:route POST /pets pet CreatePet
//
// Create a new pet.
//
// responses:
//   200: PetReply
//   default: ErrorReply

// swagger:route POST /pets/{pet_id} pet UpdatePet
//
// Update data of a pet.
//
// responses:
//   200: PetReply
//   default: ErrorReply

// swagger:route GET /pets/{pet_id} pet RetrievePet
//
// Get data of a pet.
//
// responses:
//   200: PetReply
//   default: ErrorReply

// swagger:route DELETE /pets/{pet_id} pet DeletePet
//
// Delete a pet.
//
// responses:
//   200: PetReply
//   default: ErrorReply