Я бы хотел в этой статье рассказать вам о том как можно быстро и просто сделать веб сервер на языке Golang с документацией к нему. И о том какие есть подходы и инструменты для их реализации
Сегодня мы разберем эти готовые инструменты:
Swagger Codegen
Начнем с swagger-api/swagger-codegen, представленным swagger.io. Swagger Codegen упрощает процесс сборки за счет создания серверных заглушек и клиентских SDK для любого API, определенного в спецификации OpenAPI (ранее известной как Swagger), вам остается лишь сделать реализацию вашего API.
Ниже приведен пример нашего swagger файла и его наглядный вид в Swagger Editor'е, где мы за одно можем и сгенерировать наши реализации.
{ "swagger" : "2.0", "info" : { "version" : "1.0.0", "title" : "Swagger Petstore" }, "host" : "localhost", "basePath" : "/v1", "tags" : [ { "name" : "pet" } ], "schemes" : [ "https", "http" ], "paths" : { "/pet" : { "post" : { "tags" : [ "pet" ], "summary" : "Add a new pet to the store", "operationId" : "addPet", "consumes" : [ "application/json" ], "produces" : [ "application/json" ], "parameters" : [ { "in" : "body", "name" : "body", "description" : "Pet object that needs to be added to the store", "required" : true, "schema" : { "$ref" : "#/definitions/Pet" } } ], "responses" : { "405" : { "description" : "Invalid input" } } } } }, "definitions" : { "Category" : { "type" : "object", "properties" : { "id" : { "type" : "integer", "format" : "int64" }, "name" : { "type" : "string" } } }, "Tag" : { "type" : "object", "properties" : { "id" : { "type" : "integer", "format" : "int64" }, "name" : { "type" : "string" } } }, "Pet" : { "type" : "object", "required" : [ "name", "photoUrls" ], "properties" : { "id" : { "type" : "integer", "format" : "int64" }, "category" : { "$ref" : "#/definitions/Category" }, "name" : { "type" : "string", "example" : "doggie" }, "photoUrls" : { "type" : "array", "items" : { "type" : "string" } }, "tags" : { "type" : "array", "items" : { "$ref" : "#/definitions/Tag" } }, "status" : { "type" : "string", "description" : "pet status in the store", "enum" : [ "available", "pending", "sold" ] } } } } }


У клиента и сервера идентичны объекты структур. Пример файла go-client-generated/model_pet.go и go-server-server-generated/go/model_pet.go.
/* * Swagger Petstore * * No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen) * * API version: 1.0.0 * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) */ package swagger type Pet struct { Id int64 `json:"id,omitempty"` Category *Category `json:"category,omitempty"` Name string `json:"name"` PhotoUrls []string `json:"photoUrls"` Tags []Tag `json:"tags,omitempty"` // pet status in the store Status string `json:"status,omitempty"` }
Клиент содержит так же в себе методы для отправки запросов, а сервер endpiont'ы и заглушки для их обработки.


Самый простой инструмент, в плане своего функционала. Сильная сторона, это то что можно генерировать код почти под все современные языки программирования.
go-swagger
Следующий инструмент — go-swagger/go-swagger. Он предоставляет сообществу go полный набор полнофункциональных API-компонентов для работы с Swagger API: сервер, клиент и модель данных.
- Создает сервер из спецификации Swagger
- Генерирует клиента из спецификации Swagger
- Поддерживает большинство функций, предлагаемых jsonschema и swagger, включая полиморфизм
- Создает спецификацию swagger из аннотированного кода go
- Дополнительные инструменты для работы со swagger спецификацией
- Отличные функции настройки, с расширениями поставщиков и настраиваемыми шаблонами
Сервер
Команда для генерации сервера:
$ swagger generate server -f ./swagger.json

Все файлы, кроме одного, будут перегенерированны после повторного запуска команды. Будте внимательны при обновления кода под новые версии документации.
Этот файл — go-swagger-service/restapi/configure_swagger_petstore.go. В нем мы можем конфигурировать наш сервер: определять handler'ы, подключать middleware, настраивать логирование, переопределять ответы, и все что нам может еще быть полезным.
// This file is safe to edit. Once it exists it will not be overwritten package restapi import ( "crypto/tls" "net/http" errors "github.com/go-openapi/errors" runtime "github.com/go-openapi/runtime" middleware "github.com/go-openapi/runtime/middleware" "go-generator/go-swagger-service/restapi/operations" "go-generator/go-swagger-service/restapi/operations/pet" ) //go:generate swagger generate server --target ../../go-swagger --name SwaggerPetstore --spec ../../swagger.json func configureFlags(api *operations.SwaggerPetstoreAPI) { // api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{ ... } } func configureAPI(api *operations.SwaggerPetstoreAPI) http.Handler { // configure the api here api.ServeError = errors.ServeError // Set your custom logger if needed. Default one is log.Printf // Expected interface func(string, ...interface{}) // // Example: // api.Logger = log.Printf api.JSONConsumer = runtime.JSONConsumer() api.JSONProducer = runtime.JSONProducer() if api.PetAddPetHandler == nil { api.PetAddPetHandler = pet.AddPetHandlerFunc(func(params pet.AddPetParams) middleware.Responder { return middleware.NotImplemented("operation pet.AddPet has not yet been implemented") }) } api.ServerShutdown = func() {} return setupGlobalMiddleware(api.Serve(setupMiddlewares)) } // The TLS configuration before HTTPS server starts. func configureTLS(tlsConfig *tls.Config) { // Make all necessary changes to the TLS configuration here. } // As soon as server is initialized but not run yet, this function will be called. // If you need to modify a config, store server instance to stop it individually later, this is the place. // This function can be called multiple times, depending on the number of serving schemes. // scheme value will be set accordingly: "http", "https" or "unix" func configureServer(s *http.Server, scheme, addr string) { } // The middleware configuration is for the handler executors. These do not apply to the swagger.json document. // The middleware executes after routing but before authentication, binding and validation func setupMiddlewares(handler http.Handler) http.Handler { return handler } // The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document. // So this is a good place to plug in a panic handling middleware, logging and metrics func setupGlobalMiddleware(handler http.Handler) http.Handler { return handler }
При обработке запросов происходит автоматическая валидация всех объектов и их полей. И отдается ответ со статусом 422 и описанием ошибки.
Поля валидируются на соответствие параметрам заданным в swagger документации. Такие как enum, required, type, maximum и другие.
// Code generated by go-swagger; DO NOT EDIT. package models // This file was generated by the swagger tool. // Editing this file might prove futile when you re-run the swagger generate command import ( "encoding/json" "strconv" strfmt "github.com/go-openapi/strfmt" "github.com/go-openapi/errors" "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // Pet pet // swagger:model Pet type Pet struct { // category Category *Category `json:"category,omitempty"` // id ID int64 `json:"id,omitempty"` // name // Required: true Name *string `json:"name"` // photo urls // Required: true PhotoUrls []string `json:"photoUrls"` // pet status in the store // Enum: [available pending sold] Status string `json:"status,omitempty"` // tags Tags []*Tag `json:"tags"` } // Validate validates this pet func (m *Pet) Validate(formats strfmt.Registry) error { var res []error if err := m.validateCategory(formats); err != nil { res = append(res, err) } if err := m.validateName(formats); err != nil { res = append(res, err) } if err := m.validatePhotoUrls(formats); err != nil { res = append(res, err) } if err := m.validateStatus(formats); err != nil { res = append(res, err) } if err := m.validateTags(formats); err != nil { res = append(res, err) } if len(res) > 0 { return errors.CompositeValidationError(res...) } return nil } func (m *Pet) validateCategory(formats strfmt.Registry) error { if swag.IsZero(m.Category) { // not required return nil } if m.Category != nil { if err := m.Category.Validate(formats); err != nil { if ve, ok := err.(*errors.Validation); ok { return ve.ValidateName("category") } return err } } return nil } func (m *Pet) validateName(formats strfmt.Registry) error { if err := validate.Required("name", "body", m.Name); err != nil { return err } return nil } func (m *Pet) validatePhotoUrls(formats strfmt.Registry) error { if err := validate.Required("photoUrls", "body", m.PhotoUrls); err != nil { return err } return nil } var petTypeStatusPropEnum []interface{} func init() { var res []string if err := json.Unmarshal([]byte(`["available","pending","sold"]`), &res); err != nil { panic(err) } for _, v := range res { petTypeStatusPropEnum = append(petTypeStatusPropEnum, v) } } const ( // PetStatusAvailable captures enum value "available" PetStatusAvailable string = "available" // PetStatusPending captures enum value "pending" PetStatusPending string = "pending" // PetStatusSold captures enum value "sold" PetStatusSold string = "sold" ) // prop value enum func (m *Pet) validateStatusEnum(path, location string, value string) error { if err := validate.Enum(path, location, value, petTypeStatusPropEnum); err != nil { return err } return nil } func (m *Pet) validateStatus(formats strfmt.Registry) error { if swag.IsZero(m.Status) { // not required return nil } // value enum if err := m.validateStatusEnum("status", "body", m.Status); err != nil { return err } return nil } func (m *Pet) validateTags(formats strfmt.Registry) error { if swag.IsZero(m.Tags) { // not required return nil } for i := 0; i < len(m.Tags); i++ { if swag.IsZero(m.Tags[i]) { // not required continue } if m.Tags[i] != nil { if err := m.Tags[i].Validate(formats); err != nil { if ve, ok := err.(*errors.Validation); ok { return ve.ValidateName("tags" + "." + strconv.Itoa(i)) } return err } } } return nil } // MarshalBinary interface implementation func (m *Pet) MarshalBinary() ([]byte, error) { if m == nil { return nil, nil } return swag.WriteJSON(m) } // UnmarshalBinary interface implementation func (m *Pet) UnmarshalBinary(b []byte) error { var res Pet if err := swag.ReadJSON(b, &res); err != nil { return err } *m = res return nil }
Для старта сервера используется команда
$ run ./go-swagger-service/cmd/swagger-petstore-server/main.go --port 8090
Клиент
Команда для генерации клиента:
$ swagger generate client -f ./swagger.json

Эта фича очень полезна, для написания api-sdk. Клиент содержит в себе не только модели объектов, но и методы для их преобразования в запрос, его отправку и получения ответа.
// Code generated by go-swagger; DO NOT EDIT. package pet // This file was generated by the swagger tool. // Editing this file might prove futile when you re-run the swagger generate command import ( "github.com/go-openapi/runtime" strfmt "github.com/go-openapi/strfmt" ) // New creates a new pet API client. func New(transport runtime.ClientTransport, formats strfmt.Registry) *Client { return &Client{transport: transport, formats: formats} } /* Client for pet API */ type Client struct { transport runtime.ClientTransport formats strfmt.Registry } /* AddPet adds a new pet to the store */ func (a *Client) AddPet(params *AddPetParams) error { // TODO: Validate the params before sending if params == nil { params = NewAddPetParams() } _, err := a.transport.Submit(&runtime.ClientOperation{ ID: "addPet", Method: "POST", PathPattern: "/pet", ProducesMediaTypes: []string{"application/json"}, ConsumesMediaTypes: []string{"application/json"}, Schemes: []string{"http", "https"}, Params: params, Reader: &AddPetReader{formats: a.formats}, Context: params.Context, Client: params.HTTPClient, }) if err != nil { return err } return nil } // SetTransport changes the transport on the client func (a *Client) SetTransport(transport runtime.ClientTransport) { a.transport = transport }
Есть вариант генерации документации из кода приложения. Об этом подробно можно почитать Generate a spec from source code.
Я описал не все возможность этого инструмента. Оно является самым функциональным, гибким и полным для работы с swagger и кодогенерацией.
grpc-gateway
Теперь посмотрим инструменты для генерации swagger документации. grpc-ecosystem/grpc-gateway — это плагин protoc. Он читает определение сервиса gRPC и генерирует обратный прокси-сервер, который переводит RESTful JSON API в gRPC. Этот сервер создается в соответствии с пользовательскими параметрами в вашем определении gRPC.
Этот проект направлен на предоставление интерфейса HTTP + JSON вашей службе gRPC. Это помогает вам предоставлять свои API в стиле gRPC и RESTful одновременно.

И как небольшой бонус так же есть возможность создания swagger.json с использованием protoc-gen-swagger.
Сейчас мы соберем наш сервер и пройдемся по каждому из шагов. Для этого установим наши сборщики.
$ go install \ github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway \ github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger \ github.com/golang/protobuf/protoc-gen-go
Для начала нам потребуется написать вот такой .proto файл
syntax = "proto3"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; package pet; message AddPetRequest { int64 id = 1; message Category { int64 id = 1; string name = 2; } Category category = 2; string name = 3; repeated string photo_urls = 4; message Tag { int64 id = 1; string name = 2; } repeated Tag tags = 5; //pet status in the store enum Status { available = 0; pending = 1; sold = 2; } Status status = 6; } service PetService { rpc AddPet (AddPetRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/pet" body: "*" }; } }
Выполним команду для генерации gRPC stub:
$ protoc -I/usr/local/include -I. \ -I$GOPATH/src \ -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ --go_out=plugins=grpc:. \ pet.proto
У нас появился файлик pet.pb.go. И сразу же соберем reverse-proxy с помощью protoc-gen-grpc-gateway:
$ protoc -I/usr/local/include -I. \ -I$GOPATH/src \ -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ --grpc-gateway_out=logtostderr=true:. \ pet.proto
Файл pet.pb.gw.go готов. Это и есть наш gateway. Теперь соберем pet.swagger.json утилитой protoc-gen-swagger:
$ protoc -I/usr/local/include -I. \ -I$GOPATH/src \ -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ --swagger_out=logtostderr=true:. \ pet.proto
Осталось реализовать интерфейс PetServiceServer.
// PetServiceServer is the server API for PetService service. type PetServiceServer interface { AddPet(context.Context, *AddPetRequest) (*empty.Empty, error) }
А код для старта сервера может выглядеть примерно вот так:
package pet import ( "context" "flag" "net/http" "github.com/golang/glog" "github.com/grpc-ecosystem/grpc-gateway/runtime" gw "grpc-gateway/pet" "grpc-gateway/service" ) func run() error { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() mux := runtime.NewServeMux() err := gw.RegisterPetServiceHandlerServer(ctx, mux, &service.PetService{}) if err != nil { return err } return http.ListenAndServe(":8081", mux) } func main() { flag.Parse() defer glog.Flush() if err := run(); err != nil { glog.Fatal(err) } }
Когда мы собрали минимально живучий продукт, можно перейти к тонкостям его реализации. Их вы можете посмотреть в моей отдельной статье, с более глубоким и детальным разбором подхода c инструментом grpc-gateway.
Swag
Swag преобразует аннотации Go в документацию Swagger 2.0. Написано множество плагинов для популярных веб-фреймворков Go. Это позволяет быстро интегрировать swaggo/swag в существующий проект Go (используя Swagger UI).
Поддерживаемые фреймворки:
Swagger аннотации делятся на две части, общей информации о документации и документацию endpoint'ов. Ниже приведены их примеры.
// @title Swagger Example API // @version 1.0 // @description This is a sample server celler server. // @termsOfService http://swagger.io/terms/ // @contact.name API Support // @contact.url http://www.swagger.io/support // @contact.email support@swagger.io // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @host localhost:8080 // @BasePath /api/v1 // @query.collection.format multi // @securityDefinitions.basic BasicAuth // @securityDefinitions.apikey ApiKeyAuth // @in header // @name Authorization // @securitydefinitions.oauth2.application OAuth2Application // @tokenUrl https://example.com/oauth/token // @scope.write Grants write access // @scope.admin Grants read and write access to administrative information // @securitydefinitions.oauth2.implicit OAuth2Implicit // @authorizationurl https://example.com/oauth/authorize // @scope.write Grants write access // @scope.admin Grants read and write access to administrative information // @securitydefinitions.oauth2.password OAuth2Password // @tokenUrl https://example.com/oauth/token // @scope.read Grants read access // @scope.write Grants write access // @scope.admin Grants read and write access to administrative information // @securitydefinitions.oauth2.accessCode OAuth2AccessCode // @tokenUrl https://example.com/oauth/token // @authorizationurl https://example.com/oauth/authorize // @scope.admin Grants read and write access to administrative information // @x-extension-openapi {"example": "value on a json format"} func main() { // ... }
// ShowAccount godoc // @Summary Show a account // @Description get string by ID // @ID get-string-by-int // @Accept json // @Produce json // @Param id path int true "Account ID" // @Success 200 {object} model.Account // @Header 200 {string} Token "qwerty" // @Failure 400 {object} httputil.HTTPError // @Failure 404 {object} httputil.HTTPError // @Failure 500 {object} httputil.HTTPError // @Router /accounts/{id} [get] func (c *ExampleController) ShowAccount(ctx *contex.Context) { // ... }
Также исчерпывающая документация с примерами есть в репозитории на github.
Отличный способ написания документации уже по существующим проектам, проверенным временем, расширяющимся проектам.
Вывод
Сейчас мы только что расширили наше понимание возможных взаимодействий с swagger документацией. Рассмотрели способы проектирования кода и составления документации к нему.
И я думаю, что некоторые уже выбрали подходящий способ для реализации API своего нового проекта или примерили разобранные примеры к текущим.
