GolangでAPIを作る時に見るレシピ集
はじめに
GoでAPIを作る際によく書く処理をまとめてみました。
今後も何かよく使う実装を思い付き次第、引き続き加筆・修正していく予定です。
説明の都合上、レイヤーを意識していない実装になっている箇所があるので、適宜カスタムして使ってください。
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の構造体について
以下の記事が参考になりました。
ちなみにConfig構造体は他にも色々な設定項目があります。
補足② Collationについて
Collationってなんだっけと思ったので調べました。
以下の記事が参考になります。
補足③ utf8mb4について
何となく使ってきたutf8mb4ですが、先日執筆した以下記事のとおり、UTF-8は1〜4バイト対応のはずと思って調べました。
歴史的経緯によってそうなっているようで、公式ドキュメントにもutf8ではなく、明示的にutf8mb4と指定するよう推奨されています。
参考記事。
公式ドキュメント。
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の好きなところですね。
もっと良い実装あるよ、という場合は是非コメントいただけると幸いです。
Discussion