GoでREST APIを作ります。
go-swaggerはyamlでWebAPIの定義をすることで、サーバーサイド・クライアントサイドのコードをコマンドの実行で生成してくれる。
今回はサーバーサイドだけ欲しいのでクライアントサイドは生成しない。
ただし、リクエストを受けた時にパラメータに従ってデータベースとやりとりする部分の処理(handler)は自身で実装する必要がある。
Railsとかでいうcontrollerのメソッドが中身空っぽな状態で必要なだけ自動生成され、その中身を自分で実装するみたいな感じ?
データベースはmysql。
DockerでAPI用のコンテナとデータベース用のコンテナを起動させる。
また、コード編集でホットリロードさせるためにAPIの起動にはairを使う。
- ディレクトリ構成
- Docker
- go-swagger導入
- swagger.yamlを作成
- swagger.ymlからコードを生成
- .air.tomlの編集
- ハンドラを作成
- 環境変数設定
- データベース接続
ディレクトリ構成
最終的にこうなる(ディレクトリと使うファイルのみ表示)
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
にアクセスすれば、データベースに登録した情報を取得できる。