🐁

GolangでAPIを作る時に見るレシピ集

2023/01/29に公開

はじめに

GoでAPIを作る際によく書く処理をまとめてみました。
今後も何かよく使う実装を思い付き次第、引き続き加筆・修正していく予定です。
説明の都合上、レイヤーを意識していない実装になっている箇所があるので、適宜カスタムして使ってください。

https://qiita.com/2san/items/7fd4fc50e01dd5b994d2

DBと接続したい場合

import (
	"database/sql"
	"time"

	"github.com/go-sql-driver/mysql"
)

// Close処理はmainルーチンに書く。
// このDbをimportして使い回す
var Db *sql.DB

func init() {
	jst, err := time.LoadLocation("Asia/Tokyo")
	if err != nil {
		// エラーハンドリング
	}

	// 実際には環境変数を参照するが便宜上ベタ書き
	c := mysql.Config{
		DBName:    "dbname",
		User:      "user",
		Passwd:    "pass",
		Addr:      "localhost:3306",
		Net:       "tcp",
		ParseTime: true,
		Collation: "utf8mb4_general_ci",
		Loc:       jst,
	}

	db, err := sql.Open("mysql", c.FormatDSN())

	if err != nil {
		// エラーハンドリング
	}

	Db = db
}

補足① dsnの構造体について

以下の記事が参考になりました。

https://zenn.dev/mstn_/articles/75667657fa5aed

ちなみにConfig構造体は他にも色々な設定項目があります。

https://pkg.go.dev/github.com/go-sql-driver/mysql@v1.6.0#Config

補足② Collationについて

Collationってなんだっけと思ったので調べました。
以下の記事が参考になります。

https://qiita.com/kazu56/items/6af85ffcf8d3954455ad

補足③ utf8mb4について

何となく使ってきたutf8mb4ですが、先日執筆した以下記事のとおり、UTF-8は1〜4バイト対応のはずと思って調べました。
歴史的経緯によってそうなっているようで、公式ドキュメントにもutf8ではなく、明示的にutf8mb4と指定するよう推奨されています。

https://zenn.dev/chillout2san/articles/85dfdc03ccf2c3

参考記事。

https://penpen-dev.com/blog/mysql-utf8-utf8mb4/#toc2

公式ドキュメント。

https://dev.mysql.com/doc/refman/8.0/ja/charset-unicode-utf8.html

CRUD処理のRead処理をしたい場合(一つのレコードを返す)

type User struct {
	Id   string
	Name string
	Age  int
}

func FetchUser(ctx context.Context, userId string) (User, error) {
	// RDBMSによって表記が違う。MySQLは`?`、PostgreSQLは`$1`,`$2`...で表現。
	query := `SELECT * FROM users WHERE id = ?`

	row:= Db.QueryRowContext(ctx, query, userId)

	// データベースから各値を受け取るための器を用意する
	var (
		id, name string
	)

	var (
		age int
	)

	// データベースから取ってきた値を変数にパースする
	if err := row.Scan(&id, &name, &age); err != nil {
		return User{}, err
	}

	user := User{
		Id:   id,
		Name: name,
		Age:  age,
	}

	return user, nil
}

補足 レコードがないことが考えられる場合

タイトルにもある通り、QueryRowContextメソッドはレコードが一つだけあると分かっている場合です。
なのでレコードがない場合はsql: no rows in result setと言うエラーが返ってきてしまいます。
レコードがない場合にも呼ばれる可能性がある場合は、QueryRowContextを使わずに次の複数のレコードを返すQueryContextを使いましょう。

QueryContextの例はこんな感じ。

func FetchUser(ctx context.Context, userId string) (User, error) {
	query := `SELECT * FROM users WHERE id = ?`

	// 返ってくるとしたら一つと分かっているので、rowsではなくrowと言う命名にしている
	row, err := Db.QueryContext(ctx, query, userId)

	if err != nil {
		return User{}, err
	}

	defer row.Close()

	var (
		id, name string
	)

	var (
		age int
	)

	// レコードが0件の場合、for文の中の処理に入らずエラーにならずに済む
	for row.Next() {
		if err := row.Scan(&id, &name, &age); err != nil {
			return User{}, err
		}
	}

	// id, name, ageはrow.Scanされていないので全て初期値のまま
	user := User{
		Id:   id,
		Name: name,
		Age:  age,
	}

	// userが存在するかどうかは例えばuser.Idが空文字でないかどうかで判断
	return user, nil
}

ちなみにQueryRowContextのままでこんな回避方法も無くはないと思いますが、あまりスマートではなさそうです。

func FetchUser(ctx context.Context, userId string) (User, error) {
	// 省略...
	if err := row.Scan(&id, &name, &age); err != nil {
		// レコードがないエラーの場合はエラー扱いしないように制御する
		if err.Error() == "sql: no rows in result set" {
			return User{}, nil
		}
		return User{}, err
	}
}

CRUD処理のRead処理をしたい場合(複数のレコードを返す)

type User struct {
	Id   string
	Name string
	Age  int
}

func FetchUser() (ctx context.Context, []User, error) {
	query := `SELECT * FROM users`

	rows, err := Db.QueryContext(ctx, query, userId)

	if err != nil {
		return []User{}, err
	}

	defer rows.Close()

	// 返却するuser配列の器を用意する
	var users []User

	for rows.Next() {
		var (
			id, name string
		)

		var (
			age int
		)

		if err := rows.Scan(&id, &name, &age); err != nil {
			return []User{}, err
		}

		user := User{
			Id:   id,
			Name: name,
			Age:  age,
		}

		// 配列に追加
		users = append(users, user)
	}

	return users, nil
}

CRUD処理のCreate,Update,Delete処理をしたい場合(SELECT文のようにレコードを取得しない処理)

func DeleteUser(ctx context.Context, userId string) error {
	command := `DELETE FROM users WHERE id = ?`

	// 一つ目の戻り値を使うなら受け取っても良い。
	_, err := Db.ExecContext(ctx, command, userId)

	if err != nil {
		return err
	}

	return nil
}

トランザクションをはりたい

func DeleteUser(ctx context.Context, userId string) error {
	tran, err := Db.Begin()
	if err != nil {
		return err
	}
	// エラーが起きたらロールバックするし、commitされた場合は何も起きない。
	defer tran.RollBack()

	command1 := `DELETE FROM users WHERE id = ?`

	_, err := Db.ExecContext(ctx, command1, userId)

	if err != nil {
		return err
	}

	command2 := `DELETE FROM permissions WHERE user_id = ?`

	_, err := Db.ExecContext(ctx, command2, userId)

	return tran.Commit()
}

エンドポイントのルーティングをしたい場合

レスポンスの詳しい返し方はレスポンスを返却したい場合を参照してください。

func main() {
	ExecRouter()

	// サーバを立てる
	http.ListenAndServe(":8080", nil)

	// ここでデータベースインスタンスを閉じる。
	defer Db.Close()
}

func ExecRouter() {
	http.HandleFunc("/v1/user", UserHandler)
	http.HandleFunc("/v1/book", BookHandler)
}

func UserHandler(w http.ResponseWriter, r *http.Request) {
	prefix := "/v1/user/"

	switch r.URL.Path {
	case prefix + "create":
		if r.Method != "POST" {
			// 想定のHTTPメソッドでなければ405でエラー
			w.WriteHeader(StatusMethodNotAllowed)
			w.Write([]byte("許可されていないメソッドです"))
		}
		// usecaseを呼び出す
	case prefix + "read":
		// ...
	case prefix + "update":
		// ...
	case prefix + "delete":
		// ...
	default:
		// 存在しないエンドポイントなので404で返す
		w.WriteHeader(StatusNotFound)
		w.Write([]byte("存在しないエンドポイントです"))
	}
}

func BookHandler(w http.ResponseWriter, r *http.Request) {
	// 省略
}

ちゃんとRESTfulに作るなら、リクエストのメソッドで切り替える方法もあります。

func UserHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "GET":
		// ...
	case "POST":
		// ...
	case "PUT":
		// ...
	case "PATCH":
		// ...
	case "DELETE":
		// ...
	}
}

エンドポイントに適用するmiddlewareを作りたい場合

func AddHeader(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Headers", "どうたらこうたら")
		// 他にもヘッダーを足したかったら付け加える
		next(w, r)
	}
}

func ExecRouter() {
	// こんな感じで使える。
	http.HandleFunc("/v1/user/", AddHeader(UserHandler))
}

リクエストで受け取ったjsonを構造体に変換する汎用的なmapperを作りたい場合

func ConstructInputData[T interface{}](r *http.Request, inputData T) error {
	// リクエストを読み込む。
	body, err := io.ReadAll(r.Body)
	if err != nil {
		return err
	}
	defer r.Body.Close()

	// リクエストを引数に受け取った構造体にマッピングする
	err = json.Unmarshal(body, inputData)
	if err != nil {
		return err
	}

	return nil
}

// こんな感じで使える
func UserHandler(w http.ResponseWriter, r *http.Request) {
	prefix := "/v1/user/"

	switch r.URL.Path {
	case prefix + "create":
		// usecaseに応じたinputDataの構造体を用意
		var inputData *createInputData
		// ポインタ型で渡すこと
		err := ConstructInputData(r, inputData)
		// 省略
	}
}

レスポンスを返却したい場合

func UserHandler(w http.ResponseWriter, r *http.Request) {
	prefix := "/v1/user/"

	switch r.URL.Path {
	case prefix + "create":
		var inputData *createInputData
		err := ConstructInputData(r, inputData)

		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			w.Write(err)
			return
		}

		// 色々と処理をする

		// usecaseにinputDataを渡して、結果を構造体で受け取る
		// *http.RequestはContextを持っているので、
		// ユースケース層→インフラ層と引き回す
		result, err := usecase.create(r.Context(), inputData)

		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			w.Write(err)
			return
		}

		res, err := json.Marshal(result)

		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			w.Write(err)
			return
		}

		// レスポンスを返却する
		w.WriteHeader(http.StatusOK)
		w.Write(res)
	}
}

// usecase.Createの戻り値はこんな構造体とする。
type Result struct {
	IsSuccess bool `json:"isSuccess"` // こう書くとjson上ではプロパティ名がisSuccessになる
}

ちなみにmapを使ってこんな形でも書ける。

func UserHanlder() {
	// 省略
	res, err := json.Marshal(map[string]interface{}{
		"isSuccess": result.IsSuccess,
	})
	w.Write(res)
	// 省略
}

httpクライアントとしてGoを使いたい場合

// 長くなるので、エラーハンドリングは省略してあります
// 空のスライスとエラーを返却すればよいかと思います
func FetchLogs(host string) ([]Log, error) {
	// クライアントをインスタンス化。色々設定できますが、よく使いそうなtimeoutだけ紹介。
	c := http.Client{
		Timeout: 10 * time.Second,
	}

	// リクエストを作成。第二引数はコールしたいAPIのアドレス。
	// 第三引数はPOSTメソッドのBody
	req, err := http.NewRequest("GET", host, nil)

	// リクエストを実行
	res, err := c.Do(req)

	// レスポンスを読み込む
	body, err := io.ReadAll(res.Body)

	// API構築の時同様必ず閉じる
	defer res.Body.Close()

	var result []Log // 返却したい構造体のポインタ型

	err = json.Unmarshal(body, result)

	return result, nil
}

環境変数を設定したい場合

import "github.com/caarlos0/env/v6"

type Config struct {
	// 環境変数があれば読み込むし、なければenvDefaultの値が読み込まれる
	ApiKey string `env:"API_KEY" envDefault:"dummy_api_key`
}

func NewConfig()(Config, error) {
	conf := Config{}

	// env/v6っていうパッケージ名ですが、v6.Parseとは書けないので注意。
	err := env.Parse(conf)
	if err != nil {
		return nil,err
	}
	return conf, nil
}

標準パッケージを使うとこんな感じ。
こちらはデータベースと同じく、init関数を走らせてConf変数を使い回す形にしてみました。

import "github.com/caarlos0/env/v6"

type Config struct {
	ApiKey string
}

var Conf *Config

func init() {
	conf := &Config{
		// 環境変数が設定されていなければ空文字のまま
		ApiKey: os.Getenv("API_KEY")
	}
	Conf = conf
}

おわりに

実務ではGinやGorm等のライブラリを使うケースが多いと思いますが、今回はスクラッチ気味な実装を紹介してみました。
スクラッチでも全然イケる感じするのがGoの好きなところですね。
もっと良い実装あるよ、という場合は是非コメントいただけると幸いです。

GitHubで編集を提案

Discussion