強欲で謙虚なツボツボ

趣味の読書の書の方

備忘録(go-swaggerでAPIサーバを作る、gorm, air, dockerと共に)

GoでREST APIを作ります。

go-swaggerはyamlでWebAPIの定義をすることで、サーバーサイド・クライアントサイドのコードをコマンドの実行で生成してくれる。
今回はサーバーサイドだけ欲しいのでクライアントサイドは生成しない。
ただし、リクエストを受けた時にパラメータに従ってデータベースとやりとりする部分の処理(handler)は自身で実装する必要がある。
Railsとかでいうcontrollerのメソッドが中身空っぽな状態で必要なだけ自動生成され、その中身を自分で実装するみたいな感じ?

データベースはmysql

DockerでAPI用のコンテナとデータベース用のコンテナを起動させる。
また、コード編集でホットリロードさせるためにAPIの起動にはairを使う。

 

 

ディレクトリ構成

最終的にこうなる(ディレクトリと使うファイルのみ表示)
gen以下が自動生成されたコードで、ルートディレクトリからだとgo run api/server/gen/cmd/xxx/main.goで起動できる。
gen以下の生成されたコードは編集不可だが、restapi内のconfigure_xxx.goだけは編集可能。起動時処理やミドルウェアを記述したりするもので、このファイル存在時にgo-swaggerによるコード生成を行っても上書きされないから編集することができる。

root
  ├─ api
  │    ├─ server
  │    │    ├─ gen
  │    │    │    ├─ cmd
  │    │    │    │    └─ xxx
  │    │    │    │         └─ main.go
  │    │    │    ├─ models
  │    │    │    └─ restapi
  │    │    │         ├─ operations
  │    │    │         └─ configure_xxx.go
  │    │    └─ handlers
  │    │         └─ 自分で作成したhandler
  │    ├─ swagger
  │    │    └─ swagger.yaml
  │    ├─ .air.toml
  │    ├─ go.mod
  │    ├─ go.sum
  │    └─ Dockerfile
  ├─ config
  │    └─ config.go
  ├─ db
  │    └─ Dockerfile
  └─ docker-compose.yml

 

Docker

Dockerfileとdocker-compose.ymlを作成

# api/Dockerfile
FROM golang:1.16-alpine

RUN apk update && apk add git

# ホットリロードのためにairを導入
RUN go get github.com/cosmtrek/air

WORKDIR /usr/src/app
COPY api .
RUN go mod download && go mod verify

EXPOSE 8000
# db/Dockerfile
FROM mysql:5.7

COPY db/my.conf /etc/mysql/conf.d

EXPOSE 3306
# db/my.conf
[mysqld]
character-set-server=utf8mb4
collation-server = utf8mb4_bin
general_log=1
general_log_file=/var/log/mysql/mysql.log
log-error=/var/log/mysql/mysql-error.log

[mysqld]
character-set-server=utf8mb4

[client]
character-set-server=utf8mb4
# docker-compose.yml
version: "3"

services:
  api:
    build:
      context: .
      dockerfile: api/Dockerfile
    container_name: api
    environment:
      - DB_DRIVER=mysql
      - DB_DATABASE=dev
      - DB_USER=user
      - DB_PASSWORD=password
      - DB_HOST=db
      - DB_PORT=3306
    volumes:
      - api:/usr/src/app
    ports:
      - 8000:8000
    tty: true
    command: /bin/sh -c "air -c .air.toml"
    depends_on:
      - db

  db:
    build:
      context: .
      dockerfile: db/Dockerfile
    container_name: db
    environment:
      - MYSQL_DATABASE=dev
      - MYSQL_USER=user
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=password
      - TZ=Asia/Tokyo
    ports:
      - 3306:3306

 

go-swagger導入

cd api
go mod init github.com/{githubのユーザー名}/{githubリポジトリ名}
go get -u github.com/go-swagger/go-swagger/cmd/swagger

 

swagger.yamlを作成

go-swaggerはOpenAPI2.0対応で、3.0は対応していないので注意

VScodeを使っているならば、Swagger Viewerというプラグインが便利

# api/swagger/swagger.yaml
---
swagger: "2.0"
info:
  version: 1.0.0
  title: Test
basePath: /api/v1
shemes:
  - http
paths:
  /user:
    get:
      tags:
        - user
      produces:
        - application/json
      responses:
        200:
          desciption: List of users
          schema:
            type: array
            items:
              $ref: "#/definitions/User"
        default:
          description: Unexpected error
          schema:
            $ref: "#/definitions/Error"
definitions:
  User:
    type: object
    properties:
      id:
        type: integer
        example: 1
      name:
        type: string
        example: テスト名前
      email:
        type: string
        example: test@gmail.com
      created_at:
        type: string
      updated_at:
        type: string
      deleted_at:
        type: string
  Error:
    type: object
    properties:
      code:
        type: integer
        example: 400
      message:
        type: string
        example: bad request

 

swagger.ymlからコードを生成

これによってgen以下にコードが生成される

cd api
mkdir -p server/gen
swagger generate server --strict-additional-properties -t ./server/gen -f ./swagger/swagger.yaml

 

.air.tomlの編集

起動するのに生成されたmain.goを参照するように変更し、hostとportを指定する
hostはデフォルトが127.0.0.0でDockerコンテナ外からアクセスするのに不便なので0.0.0.0を指定
portは起動するたびにランダムでホットリロードされるたびにポートが変化して面倒なので8000を指定

# api/.air.toml
cmd = "go build -o ./tmp/main ./server/gen/cmd/xxx"
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main --host 0.0.0.0 --port 8000"

 

ハンドラを作成

この時点でdocker-compose up -dで起動できるが、localhost:8000/api/v1/userにアクセスすると「operation user.GetUser has not yet been implemented」と返ってくる
自動生成されたコードを見るとapi/server/gen/restapi/configure_xxx.goでapi.UserGetUserHandlerが定義されていないことがわかる。
なのでハンドラを作成して設定する。

# api/server/handers/user.go
type UserHandler struct {}

func NewUserHandler() UserHandler {
    return UserHandler{}
}

func (uh *UserHandler) GetUser(params user.GetUserParams) middleware.Responder {
    ver us []*models.User
    return user.NewGetUserOK().WithPayload(us)
}
# api/server/gen/restapi/configure_xxx.go
# configureAPI内に下記を追加
uh := handlers.NewUserHandler()
api.UserGetUserHandler = user.GetUserHandlerFunc(uh.GetUser)

これでlocalhost:8000/api/v1/userにアクセスするとhanderのWithPayloadで設定した空の配列が返ってくる

 

環境変数設定

docker-compose.ymlのenvironmentに記述した環境変数を読み込んでおく

# api/config/config.go
package config

type Config struct {
    DbDriver   string
    DbDatabase string
    DbUser     string
    DbPassword string
    DbHost     string
    DbPort     string
}

func NewConfig() Config {
    return Config{
        DbDriver:   os.Getenv("DB_DRIVER"),
        DbDatabase: os.Getenv("DB_DATABASE"),
        DbUser:     os.Getenv("DB_USER"),
        DbPassword: os.Getenv("DB_PASSWORD"),
        DbHost:     os.Getenv("DB_HOST"),
        DbPort:     os.Getenv("DB_PORT"),
    }
}

 

データベース接続

dockerで起動しているdbへ接続する。
接続情報は先の環境変数
今回はgormを使用する。

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
# app/database/database.go
package database

type DatabaseClient struct {
    c *config.Config
}

func NewDatabaseClient(config *config.Config) DatabaseClient {
    return DatabaseClient{
        c: config,
    }
}

func (dc *DatabaseClient) ConnectDatabaseClient() *gorm.DB {
    driver := dc.c.DbDriver
    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        dc.c.DbUser,
        dc.c.DbPassword,
        dc.c.DbHost,
        dc.c.DbPort,
        dc.c.DbDatabase,
    )
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("Failed to connect database: ", err.Error())
    }
    return db
}

func (dc *DatabaseClient) CloseDatabaseClient(db *gorm.DB) {
    sqlDb, _ := db.DB()
    if err := sqlDb.Close(); err != nil {
        log.Fatal("Failed to close connection database: ", err.Error())
    }
}
# api/server/gen/restapi/configure_xxx.go
# configureAPIを編集(下記は編集後の最終形態)
func configureAPI(api *operations.xxxAPI) http.Handler {
    // 下記を追記
    c := config.NewConfig()
    dc := database.NewDatabaseClient(&c)
    db := dc.ConnectDatabaseClient()
    
    // さっきhandler作成の時に追加した記述(引数にdbを持たせる)
    uh := handlers.NewUserHandler(db)
    api.UserGetUserHandler = user.GetUserHandlerFunc(uh.GetUser) 
    
    // 以下は初期のまま
    api.ServerError = errors.ServerError
    api.UseSwaggerUI() 
    api.JSONConsumer = runtime.JSONConsumer()
    api.JSONProducer = runtime.JSONProducer() 
    api.PreServerShutdown = func() {} 
    
    // CloseDatabaseClientを追記 
    api.ServerShutdown = func() { 
        dc.CloseDatabaseClient(db) 
    }
    
    return setupGlobalMiddleware(api.Serve(setupMiddleware)) }
# api/server/handlers/user.go
# データベースから情報を取得するように編集(下記は編集後の最終形態)

# DBを操作できるように変更
type UserHandler struct {
    db *gorm.DB
}

# 引数にdbを受け取るように変更
func NewUserHandler(db *gorm.DB) UserHandler {
    return UserHandler{
        db: db,
    }
}

# DBからデータを取得するように変更
func (uh *UserHandler) GetUser(params user.GetUserParams) middleware.Responder {
    ver us []*models.User
    res := uh.db.Find(&us)
    if err := res.Error; err != nil {
        return user.NewGetUserDefault(500)
    }
    return user.NewGetUserOK().WithPayload(us)
}

 

これでlocalhost:8000/api/v1/userにアクセスすれば、データベースに登録した情報を取得できる。