Go API開発の環境構築 テンプレート アーキテクチャ、ホットリロード、ロギング、Docker

GoでAPIを作るときのテンプレです。 ホットリロード、ロギング、コンテナ、DB、まとめて構築していきます。

以下の技術を使用します。

ディレクトリ構成です。

├── Dockerfile
├── air.toml //airの設定
├── config
│   ├── config.go
│   └── development.yml
├── db
│   └── my.cnf
├── docker-compose.yml
├── entry.sh
├── go.mod
├── go.sum
├── main.go
├── src
│   └── infrastructure
│       ├── db
│       │   └── sqlhandler.go
│       ├── logging
│       │   └── logging.go
│       └── router
│           └── router.go
└── tmp
    └── main

クリーンアーキテクチャでの実装を前提としています。

Dockerfile

Dockerfile

FROM golang:latest as development

WORKDIR /app
VOLUME /app
ADD . /app

ENV GO_ENV=development

RUN apt-get update && apt-get -y install default-mysql-client
RUN go get github.com/cosmtrek/air

CMD ["./entry.sh"]

docker-compose側でマルチビルドしたいので as developmentとします。 開発環境用なので環境変数に developmentを渡します。 buildに必要な mysql clientと airをインストールします。

docker-compose

version: '3.7'
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: development
    ports:
      - "8080:8080"
    volumes:
      - "./:/app"
    security_opt:
      - seccomp:unconfined
    depends_on:
      - db
    environment:
      DB_PORT: 3306
      DB_NAME: sample_pj
      DB_USER: root
      DB_PASSWORD: password
  db:
    platform: linux/x86_64 #for m1Tip
    image: mysql:8.0.25
    environment:
      MYSQL_DATABASE: sample_pj
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    volumes:
      - ./data/mysql:/var/lib/mysql:cached
      - ./db/my.cnf:/etc/mysql/conf.d/my.cnf

m1チップのMac Bookではplatform: linux/x86_64を指定する必要があります。

entry.sh

#!/bin/sh
set -e

$GOPATH/bin/air -c air.toml

set -e https://qiita.com/youcune/items/fcfb4ad3d7c1edf9dc96#set--e -cオプション はコンフィグファイルの指定https://github.com/cosmtrek/air#usage

air.toml

root = "."
tmp_dir = "tmp"

[build]
cmd = "go build -o ./tmp/main ./main.go"
bin = "tmp/main"

次にGoのコードを書いていきます。

config/config.go

package config

import (
    "fmt"
    "io/ioutil"
    "log"
    "os"
    "sync"

    "github.com/caarlos0/env"
    "gopkg.in/yaml.v2"
)

type Config struct{
    Port string
    Db Db
}

type Db struct {
    Port string `env:"DB_PORT"`
    Name string `env:"DB_NAME"`
    User string `env:"DB_USER"`
    Password string `env:"DB_PASSWORD"`
}

var config *Config
var once sync.Once

func Get() *Config {
    return config
}

func init() {
    once.Do(func(){
        config = &Config{}
        goenv := os.Getenv("GO_ENV")
        filepath := fmt.Sprintf("./config/%v.yml", goenv)
        buf, err := ioutil.ReadFile(filepath)
        if err != nil {
            log.Fatalln("failed to load config yaml err:", err)
        }

        fmt.Println("config:", config)
        err = yaml.Unmarshal(buf, config)
        if err != nil {
            log.Fatalln("failed to Unmarshal yaml err: ", err)
        }

        if err := env.Parse(&config.Db); err != nil {
            fmt.Printf("failed to load Db error: %s", err)
        }
    })
}

ここでは環境変数の設定を行います。 環境変数はDockerfileとdocker-composeの両方から渡しています。 os.Getenvで読み込む GO_ENVはDockerfile env アノテーションで読み込むDB_ はdocker-composeからです。

ioutil.ReadFileでconfig配下のymlファイルを読み込みます。

development.yml

port: 8080

これでGO_ENV環境変数毎にymlを切り替えます。

次にsrc配下のGoファイルを作っていきます。 今回はクリーンアーキテクチャを前提にFrameworks & Drivers に当たるinfrastructureの部分を実装していきます。

f:id:shikatech:20210526095810j:plain

src/infrastructure/db/sqlhandler.go

package db

import (
    "fmt"
    "log"

    "github.com/username/sample_pj/config"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

type SQLHandler struct {
    Conn *gorm.DB
}

func New() *SQLHandler {
    connectionString := genConnectString()
    fmt.Println("connectionString: ", connectionString)
    conn, err := gorm.Open("mysql", connectionString)
    if err != nil {
        log.Fatal("db connection error: ", err)
    }
    fmt.Println("Connection OK")
    conn.LogMode(true) // debug mode
    sqlHandler := &SQLHandler{
        Conn: conn,
    }

    return sqlHandler
}

func genConnectString() string {
    conf := config.Get()
    USER := conf.Db.User
    PASSWORD := conf.Db.Password
    PROTOCOL := fmt.Sprintf("tcp(db:%s)", conf.Db.Port)
    DBNAME := conf.Db.Name
    QUERY := "?charset=utf8mb4&parseTime=True&loc=Local"

    return USER + ":" + PASSWORD + "@" + PROTOCOL + "/" + DBNAME + QUERY
}

*gorm.DBを返します。

src/infrastructure/logging/logging.go

package logging

import (
    "fmt"
    "io"
    "log"
    "os"
)


type Logger struct {
    F *log.Logger
    I *log.Logger
    D *log.Logger
    E *log.Logger
}

type NullWriter struct{}

func (n NullWriter) Write(p []byte) (int, error) {
    return 0, nil
}

func NewLogger(env string) *Logger {
    var fatalWriter io.Writer = os.Stderr
    var infoWriter io.Writer = os.Stdout
    var errorWriter io.Writer = os.Stderr
    var debugWriter io.Writer = os.Stdout

    if env != "development" {
        debugWriter = NullWriter{}
    }

    return &Logger{
        F: log.New(fatalWriter, "[FATAL] ", log.Llongfile|log.Ldate|log.Lmicroseconds),
        E: log.New(errorWriter, "[ERROR] ", log.Llongfile|log.Ldate|log.Lmicroseconds),
        I: log.New(infoWriter, "[INFO]  ", log.Llongfile|log.Ldate|log.Lmicroseconds),
        D: log.New(debugWriter, "[DEBUG] ", log.Llongfile|log.Ldate|log.Lmicroseconds),
        }
}

func (l *Logger) Info(v ...interface{}) {
    l.I.Output(2, fmt.Sprint(v...))
}

func (l *Logger) Infof(format string, v ...interface{}) {
    l.I.Output(2, fmt.Sprintf(format, v...))
}

func (l *Logger) Debug(v ...interface{}) {
    l.D.Output(2, fmt.Sprint(v...))
}

func (l *Logger) Debugf(format string, v ...interface{}) {
    l.D.Output(2, fmt.Sprintf(format, v...))
}

func (l *Logger) Error(v ...interface{}) {
    l.E.Output(2, fmt.Sprint(v...))
}

func (l *Logger) Errorf(format string, v ...interface{}) {
    l.E.Output(2, fmt.Sprintf(format, v...))
}

func (l *Logger) Fatal(v ...interface{}) {
    l.F.Output(2, fmt.Sprint(v...))
    os.Exit(1)
}

func (l *Logger) Fatalf(format string, v ...interface{}) {
    l.F.Output(2, fmt.Sprintf(format, v...))
    os.Exit(1)
}

標準logライブラリのマッパーです。 外部パッケージでlogger.Debug("debug")のように使用できます。 可変引数についてhttps://golang.org/ref/spec#Passing_arguments_to_..._parameters logについて https://waman.hatenablog.com/entry/2017/09/29/011614

src/infrastructure/router/router.go

package router

import (
    "net/http"

    "github.com/username/sample_pj/config"
    "github.com/username/sample_pj/src/infrastructure/db"
    "github.com/gin-gonic/gin"
)

func Dispatch(sqlhandler *db.SQLHandler) {
    conf := config.Get()

    r := gin.Default()
    r.GET("/hello", func(ctx *gin.Context) {
        ctx.JSON(http.StatusOK, gin.H{
            "message": "hello",
        })
    })

    r.Run(":" + conf.Port)
}

/hello にアクセスした時, {"message": "hello"} を返します。

main.go

package main

import (
    "github.com/username/sample_pj/src/infrastructure/db"
    "github.com/username/sample_pj/src/infrastructure/logging"
    "github.com/username/sample_pj/src/infrastructure/router"
)

func main() {
    logger := logging.NewLogger("development")
    logger.Debug("debug log")

    sqlhandler := db.New()
    defer sqlhandler.Conn.Close()

    router.Dispatch(sqlhandler)
}
docker-compose up

以上。