58
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GolangでAPIを作るレシピ集

Last updated at Posted at 2022-07-24

はじめに

最近業務でGolangを触ることが増えてきました。
「この実装前もやったな」とか「これどうやって書くんだっけ?」ということが増えてきたので、レシピっぽくまとめてみました。
「APIを作るレシピ集」としてますが、色々書いてます。

2023年3月14日追記
ありがたいことに未だにちょこちょこLGTM頂けています。
Zennにクオリティアップした新しいバージョンを投稿していますので、こちらも是非ご確認ください。

DBと接続したい場合

/*
DB変数を用意しておいて、それを使いたいところでimportする。
importした段階でinitメソッドを走らせるという実装が多い。
DDDでやるならインフラ層に記述する。
*/

import (
	"database/sql"
	"fmt"
	"os"

	// mysqlのドライバーをimportする。
	_ "github.com/go-sql-driver/mysql"
)

func init() {
	// dsn(データソースネーム)を構築。環境変数で渡すならos.Getenvで渡す。
	dsn := fmt.Sprintf("%s:%s@tcp(db:3306)/%s?charset=utf8", os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_DATABASE"))

	// 第一引数がドライバーの名前、第二引数がdsn(データソースネーム)。このDbを各所で使う。
	Db, err := sql.Open("mysql", dsn)

	if err != nil {
		fmt.Printf("Fail to Open Db:%v\n", err)
		return
	}

	// DbのClose処理はmainルーチンに書くことが多いです。
}

CRUD処理のRead処理をしたい場合(一つのオブジェクトだけ返す)

/*
こんなjsonを返すエンドポイントを作りたい
{
	"id": "ABCDEFG..."
	"name": "taro",
	"age": 20
}
*/

// 本来はドメイン層に記述
type User struct {
	Id   string
	Name string
	Age  int
}

func FetchUser(userId string) (User, error) {
	// データベースによって?だったり$1だったり表記が違う。MySQLは?、PostgreSQLは$1,$2...で表現。
	query := `SELECT * FROM users WHERE id = ?`

	// クエリ実行
    // ちなみにcontext.Contextを渡して、Db.QueryRowContext(ctx, query, userId)にするのが主流っぽい。
	row, err := Db.QueryRow(query, userId)

	if err != nil {
		// エラーなので空のインスタンスとエラーを返却する
		return User{}, err
	}

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

	var (
		age int
	)

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

	// インスタンス化する
	user := User{
		Id:   id,
		Name: name,
		Age:  age,
	}

	return user, nil
}

// DDD等でコンストラクタメソッドを作ってそこでバリデーションかけているなら、ここで呼ぶ。
func FetchUser() {
    // 省略
	user, err := NewUser(id, name, age)
	if err != nil {
		return User{}, err
	}
	return user, nil
}

CRUD処理のRead処理をしたい場合(配列を返す)

/*
こんなjsonを返すエンドポイントを作りたい
[
	{
		"id": "ABCDEFG..."
		"name": "taro",
		"age": 20
	},
	...他のオブジェクト
]

*/

type User struct {
	Id   string
	Name string
	Age  int
}

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

	rows, err := Db.Query(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処理をしたい場合

func DeleteUser(userId string) error {
	command := `DELETE FROM users WHERE id = ?`

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

	if err != nil {
		return err
	}

	return nil
}

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

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

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

	_, err := Db.Exec(command1, userId)

	if err != nil {
		return err
	}

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

	_, err := Db.Exec(command2, userId)

	return tran.Commit()
}

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

// main.goに書く
func main() {
	ExecRouter()

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

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

// router.goに書く
func ExecRouter() {
	// ドメインモデル毎にHandleFuncを分けておく
	http.HandleFunc("/v1/user/", UserHandler)
	http.HandleFunc("/v1/book/", BookHandler)
}

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

	switch r.URL.Path {
	case prefix + "create":
		// usecaseを呼び出す
	case prefix + "read":
		// どうたらこうたら
	case prefix + "update":
		// どうたらこうたら
	case prefix + "delelte":
		// どうたらこうたら
	}
}

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

  • http.HanlderFuncはメソッドではなくて型です。
  • 実装を覗くと、type HandlerFunc func(w http.ResponseWriter, r *http.Requestとあります。
  • なのでキャストする必要はないのですが、HandlerFuncを返しているんだよ、が分かりやすいようにキャストしています。
/*
めっちゃこんがらがるので、イディオムとして覚えておいた方が良いかも知れない。
http.HandleFuncの第二引数はhttp.HandlerFunc型なので、http.HandlerFuncを受け取って、http.HandlerFuncを返却すれば良い
ややこしいが、http.HandleFuncの第二引数がhttp.HandlerFunc型。http.HandleFuncがhandlerだから、と考えるとわかりやすいかも。
*/

func AddHeader(next http.HandlerFunc) http.HandlerFunc {
	return http.HandlerFunc(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) {
	// リクエストを読み込む。
	body, err := io.ReadAll(r.Body)
	if err != nil {
		fmt.Println(err)
	}
	// 必ず閉じる。
	defer r.Body.Close()

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

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

	switch r.URL.Path {
	case prefix + "create":
		// usecaseに応じたinputDataの構造体を用意
		var inputData *createInputData
		ConstructInputData(r, &inputData)

	case prefix + "read":
		// 他のusecaseでもよしなにマッピングしてくれます
		var inputData *readInputData
		ConstructInputData(r, &inputData)
	}
}

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

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

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

		// 色々と処理をする

		// usecaseにinputDataを渡して、結果を構造体で受け取る
		result, err := create(inputData)

		// サーバ起因のエラーの場合
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			w.Write(err)
			return
		}

		// レスポンス用にbyte配列に変える
		res, err := json.Marshal(result)

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

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

// ちなみにエラーは構造体の形にしておくとフロント側でオブジェクトで見れるのでありがたい
type ErrResult struct {
	IsSuccess bool
	Message   string
}

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

	switch r.URL.Path {
	case prefix + "create":
		// 省略

		// usecase内の処理でコケたとする
		result, errResult := create(inputData)

		if !errResult.IsSuccess {
			w.WriteHeader(http.StatusInternalServerError)
			errRes, err := json.Marshal(errResult)

			if err != nil {
				// Marshalでコケると文字列で返すしかない。
				w.Write(err)
                return
			}
			w.Write(errRes)
			return
		}

		// 省略
	}
}
  • ちなみにエラー用の構造体を作る程でもないなら、map使うのも手。
func UserHanlder() {
  // 省略

  if err != nil {
    // json.Marshalの第二引数のエラーを無視している実装をチラホラ見る。みなさんどうしてるんだろう。
    // 一応エラーハンドリング書いておく派です。
    errRes, _ := json.Marshal(map[string]interface{}{
        "result": false,
        "err_message": err.Error()
    })
  }
  // 省略
}

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

func fetchLogs(host string) ([]Log, error) {
	// クライアントをインスタンス化。色々設定できますが、よく使いそうなtimeoutだけ紹介。
	c := http.Client{
		Timeout: 10 * time.Second,
	}

	// リクエストを作成。第三引数は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, err
}

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

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

type Config struct {
	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
}

おわりに

もっと良い実装あるよ、という場合は是非コメントいただけると幸いです。
今度は普段何気なく使っているパッケージが何をしているか調べてみる記事とか書きたいな〜。
io.ReadAllとかjson.Marshallとかノリで使ってしまっているので・・・。

58
55
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
58
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?