はじめに
最近業務で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
とかノリで使ってしまっているので・・・。