MORITOMOMENT

登山好きエンジニアのテックブログ

プログラミング・アウトドア関連を中心に発信

Golang (net/http)とFirestoreで簡単なREST APIを作ってみた。

1. 概要

最近GolangAPIの開発をする機会があり、GinなどのAPIフレームワークを使わずに標準ライブラリのnet/httpのみで開発をおこないました。 フレームワークの場合はGETやPOSTのハンドリングをよしなにやってくれますが、net/httpでは明示的にhttp methodをハンドリングする処理を書く必要がありました。

今回はユーザのCRUD機能を持つだけの簡単なREST APIのサンプルプログラムを元に、 上記開発の機会から学んだことをアウトプットしようというモチベーションで本記事を書きました。

今回の内容

  • Golangで基本的なCRUDをするREST APIを作成
  • net/http 使用
  • EchoやGinなどのframeworkは使用しない
  • DBはfirestoreを使用
  • 簡単なエラーハンドリング

今回作るもの

  • ユーザのCRUD機能を作成する
    • GET /api/users : Userリストを取得
    • GET /api/users/:userId :特定のUserを取得する
    • POST /api/users:Userを作成
    • POST /api/users/:userId:Userを更新
    • DELETE /api/users/:userId :Userを削除
  • ユーザの属性
  • CRUDするデータソースにはFirestoreを使用
    • firestoreにドキュメント作成・更新・削除のクエリを投げる

注意事項

まだ筆者はGolangの経験が浅いのでerror handlingやlogなど実務上甘い点が多々あるかもしれないのでアドバイスありましたらコメントいただければと思いますmm

それとソースコード一部のみしか抜粋して記事に載せていないので適宜下記のサンプルコードと見合わせてご覧いただくといいと思います。

実際に筆者が作ったサンプルコードはこちら

github.com

2. 環境設定

project設定

module pathを指定してgo mod initを実行し

# 例 go mod init github.com/MoriTomo7315/go-user-rest-api
go mod init github.com/<your_git_repo_name>/<project_name>

必要なモジュールをgetする

go get cloud.google.com/go/firestore
go get firebase.google.com/go
go get github.com/joho/godotenv # .env使用するためのモジュール

今回作ろうとしているAPIディレクトリ構造

下記のようにDDD(ドメイン駆動開発)のレイヤードアーキテクチャを意識して設計しました。

.
├── Dockerfile
├── README.md
├── application # アプリケーション層 (ビジネスロジックを書くところ)
│   ├── user.go
│   ├── user_test.go
│   └── util
│       ├── error.go
│       └── response.go
├── controller # コントローラー層 (http requestのハンドリングの処理を書くところ)
│   └── user.go
├── domain # ドメイン層
│   ├── define
│   │   └── error.go
│   ├── model
│   │   ├── error.go
│   │   ├── response.go
│   │   └── user.go
│   └── repository
│       └── firestore.go
├── firebase_credential.json
├── go.mod
├── go.sum
├── infrastructure # インフラ層
│   └── persistence
│       ├── firestore.go
│       └── firestore_test.go
└── main.go

2. 全体像

f:id:moritomo7315:20220203231013p:plain

3. controller層実装

controller層はhttp requestをclientから受け取るときに、このリクエストがGET methodであれば、User情報取得の処理に割り振り、POST methodであればUser作成、更新処理に割り振る、といったような制御を目的とした層です。なのでリクエストを受け取ったら最初に処理がおこなわれる部分となります。もちろんこのcontroller層を用いてGET, POST以外のmethodを受け付けないといったような制御も可能となります(本来はweb serverで制御すべきかもしれないがアプリ側でも制御はできる)

controller層のインターフェース

controller層は2つのインターフェースを持ち、main.go (serverにあたるもの)によって使用されます。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/controller/user.go#L11-L13

type UserController interface {
    HandlerHttpRequest(w http.ResponseWriter, r *http.Request)
    HandlerHttpRequestWithParameter(w http.ResponseWriter, r *http.Request)
}

HandlerHttpRequestとHandlerHttpRequestWithParameterの違い

HandlerHttpRequestHandlerHttpRequestWithParameterの2つのメソッドを用意している理由は、

例えばユーザ情報を全取得したいときに下記のようにリクエストされることが考えられます。

HandlerHttpRequestWithParameterだけだと、例えばlocalhost:/50001/api/usersでアクセスするとそのようなurlをハンドリングする処理はないためエラーとなってしまいます。

なのでControllerの各2つのmethodは下記のようにHTTP Requestを捌いています。

  • GET /api/users, POST /api/users
    • HandlerHttpRequest()
  • GET /api/users/, GET /api/users/:user_id, POST /api/users/, POST/api/users/:user_id, DELETE /api/users/:user_id
    • HandlerHttpRequestWithParameter()

詳細な実装

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/controller/user.go#L26-L89

func (uc *userController) HandlerHttpRequest(w http.ResponseWriter, r *http.Request) {
    log.Printf("INFO [/api/users] START ===========")
    switch r.Method {
    case http.MethodGet:
        /*
           全Userを取得する
       */
        uc.userApplication.GetUsers(w, r)
    case http.MethodPost:
        /*
           Userを作成する
       */
        uc.userApplication.CreateUser(w, r)
    default:
        /*
           GET, POST以外のhttp methodは許可しない
       */
        w.WriteHeader(405)
    }
    log.Printf("INFO [/api/users] END ===========")
}



func (uc *userController) HandlerHttpRequestWithParameter(w http.ResponseWriter, r *http.Request) {
    log.Printf("INFO [/api/users/] START ===========")
    userId := strings.TrimPrefix(r.URL.Path, "/api/users/")
    switch r.Method {
    case http.MethodGet:
        if len(userId) == 0 {
            /*
               /api/users/でリクエストが来た場合は/api/userと同じ処理をする(リダイレクト的役割)
           */
            uc.userApplication.GetUsers(w, r)
        } else {
            /*
               /api/users/:userIdでリクエストが来た場合はidがuserIdのuser情報を返す
           */
            uc.userApplication.GetUserById(w, r, userId)
        }
    case http.MethodPost:
        if len(userId) == 0 {
            /*
               /api/users/でリクエストが来た場合は/api/userと同じ処理をする(リダイレクト的役割)
           */
            uc.userApplication.CreateUser(w, r)
        } else {
            /*
               /api/users/:userIdでリクエストが来た場合はidがuserIdのuser情報を更新する
           */
            uc.userApplication.UpdateUser(w, r, userId)
        }
    case http.MethodDelete:
        if len(userId) == 0 {
            w.WriteHeader(400)
        } else {
            uc.userApplication.DeleteUser(w, r, userId)
        }
    default:
        /*
           GET, POST, DELETE以外のhttp methodは許可しない
       */
        w.WriteHeader(405)
    }
    log.Printf("INFO [/api/users/] END ===========")
}

http methodのハンドリング

Controller層はnet/httpのHttpRequest型でhttp requestを受け取ります。 各関数はhttp requestを r として受けとっており、"r.Method" でhttp methodが何かを参照し、どのビジネスロジックに振り分けるかを判断します。

http methodの判定にはnet/httpの定数に、http.MethodGet, http.MethodPostなどが用意されているため、明示的に"GET", "POST"と書くのではなく、net/httpモジュールの定数を使用することが好ましそうです。

pkg.go.dev

/api/users/:user_idのuser_idの取り出しについて

下記がurlからuser_idを取り出す処理に該当しますが、stringsモジュールというもののTrimPrefixというメソッドを使用しています。 Prefix(接頭辞)をトリムするという、文字通りのメソッドで、/api/users/hogehoge から/api/users/をトリムして、hogehogeがuserIdに代入されることとなります。

userId := strings.TrimPrefix(r.URL.Path, "/api/users/")

4. application層の実装

application層は受け取ったhttp request bodyの詳細なバリデーションチェックや、ユーザの存在チェック、firestoreから取得したユーザデータをレスポンスモデルにマッピングするといったビジネスロジックを実装しています。

5節のinfrastructure層で詳細は後述しますが、 application層からユーザ情報を取得する際にはdomain/repository/firestore.goで定義するFirestoreRepositoryのメソッドを使用しています。FirestoreRepositoryにfirestoreへのユーザ情報取得や作成のためのクエリのinterfaceを持っているイメージです。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L23-L25

type userApplication struct {
    firestoreRepository repository.FirestoreRepository
}

FirestoreRepositoryの詳細はこちらをご覧ください。

github.com

ビジネスロジックの一覧

application層のインターフェースとして下記を持っています。 それぞれの役割については名前から理解できると思うので省略します。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L14-L21

// インターフェース
type UserApplication interface {
    GetUsers(w http.ResponseWriter, r *http.Request)
    GetUserById(w http.ResponseWriter, r *http.Request, userId string)
    CreateUser(w http.ResponseWriter, r *http.Request)
    UpdateUser(w http.ResponseWriter, r *http.Request, userId string)
    DeleteUser(w http.ResponseWriter, r *http.Request, userId string)
}

全ユーザ情報取得

全ユーザ情報を取得するビジネスロジックGetUsers(w http.ResponseWriter, r *http.Request)に実装しました。 GET /api/users or /api/users/のときに、このインターフェースはController層から使用されます。

このインターフェースの実装は以下のようになります。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L34-L47

// ユーザ一覧取得
func (ua userApplication) GetUsers(w http.ResponseWriter, r *http.Request) {
    log.Printf("INFO [GetUsers] Application logic start")
        // firestoreからユーザ情報を取得する
    users, err := ua.firestoreRepository.GetUsers()
        // エラーチェック
    if err != nil {
        util.CreateErrorResponse(w, err, "")
        return
    }
        // エラーがない場合はレスポンスを作成する
    resModel := util.GetResponse(http.StatusOK, "ユーザ情報取得に成功しました。", int64(len(users)), users)
    res, _ := json.Marshal(resModel)

    w.Header().Set("Content-Type", "application/json")
    w.Write(res)
}

特定のユーザ情報取得

特定のユーザ情報を取得するビジネスロジックGetUserById(w http.ResponseWriter, r *http.Request, userId string)に実装しました。 GET /api/users/:user_idのときに、このインターフェースはController層から使用されます。

このインターフェースの実装は以下のようになります。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L49-L63

func (ua userApplication) GetUserById(w http.ResponseWriter, r *http.Request, userId string) {
    log.Printf("INFO [GetUserById] Application logic start")
        // firestoreからuserIdを指定してユーザ情報を取得する
    user, err := ua.firestoreRepository.GetUserById(userId)
        // エラーチェック
    if err != nil {
        util.CreateErrorResponse(w, err, userId)
        return
    }
        // エラーがない場合はレスポンスを作成する
    users := []*model.User{user}
    resModel := util.GetResponse(http.StatusOK, "ユーザ情報取得に成功しました。", int64(len(users)), users)
    res, _ := json.Marshal(resModel)

    w.Header().Set("Content-Type", "application/json")
    w.Write(res)
}

ユーザ情報の作成

ユーザ情報を作成するビジネスロジックCreateUser(w http.ResponseWriter, r *http.Request)に実装しました。 POST /api/users or /api/users/のときに、このインターフェースはController層から使用されます。

このインターフェースの実装は以下のようになります。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L65-L82

func (ua userApplication) CreateUser(w http.ResponseWriter, r *http.Request) {
    log.Printf("INFO [CreateUser] Application logic start")
        // http request bodyを取得
    body, _ := ioutil.ReadAll(r.Body)
        // http request bodyをUserModelにmapping
    var user *model.User
    json.Unmarshal(body, &user)
        // firestore上にユーザ情報を登録
    err := ua.firestoreRepository.CreateUser(user)
        // エラーチェック
    if err != nil {
        util.CreateErrorResponse(w, err, "")
        return
    }
        // エラーがない場合はレスポンスを作成する
    resModel := util.GetResponse(http.StatusCreated, "作成に成功しました。", 0, nil)
    res, _ := json.Marshal(resModel)
    w.Header().Set("Content-Type", "application/json")
    w.Write(res)
}

ユーザ情報の更新

ユーザ情報を更新するビジネスロジックUpdateUser(w http.ResponseWriter, r *http.Request, userId string)に実装しました。 POST /api/users/:user_idのときに、このインターフェースはController層から使用されます。

このインターフェースの実装は以下のようになります。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L84-L108

func (ua userApplication) UpdateUser(w http.ResponseWriter, r *http.Request, userId string) {
    log.Printf("INFO [UpdateUser] Application logic start")
    // user存在 チェック
    _, err := ua.firestoreRepository.GetUserById(userId)
    if err != nil {
        util.CreateErrorResponse(w, err, userId)
        return
    }

         // http request bodyを取得
    body, _ := ioutil.ReadAll(r.Body)

    var newUser *model.User
        // request bodyはjson形式のためencoding/jsonでUserModelにマッピングできる
    json.Unmarshal(body, &newUser)
    newUser.Id = userId
        // firestore上のユーザ情報を更新
    err = ua.firestoreRepository.UpdateUser(newUser)
    if err != nil {
        util.CreateErrorResponse(w, err, userId)
        return
    }

    resModel := util.GetResponse(http.StatusNoContent, "更新に成功しました。", 0, nil)
    res, _ := json.Marshal(resModel)
    w.Header().Set("Content-Type", "application/json")
    w.Write(res)
}

ユーザ情報の削除

ユーザ情報を更新するビジネスロジックDeleteUser(w http.ResponseWriter, r *http.Request, userId string)に実装しました。 DELETE /api/users/:user_idのときに、このインターフェースはController層から使用されます。

このインターフェースの実装は以下のようになります。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L110-L130

func (ua userApplication) DeleteUser(w http.ResponseWriter, r *http.Request, userId string) {
    log.Printf("INFO [DeleteUser] Application logic start")
    // user存在 チェック
    _, err := ua.firestoreRepository.GetUserById(userId)
    if err != nil {
        util.CreateErrorResponse(w, err, userId)
        return
    }
        // firestore上のユーザ情報を削除
    err = ua.firestoreRepository.DeleteUser(userId)
    if err != nil {
        util.CreateErrorResponse(w, err, userId)
        return
    }

    resModel := util.GetResponse(http.StatusNoContent, "削除に成功しました。", 0, nil)
    res, _ := json.Marshal(resModel)
    w.Header().Set("Content-Type", "application/json")
    w.Write(res)
}

5. infrastructrure層の実装

infrastructure層ではdomain層がapplication層に提供しているinterfaceの詳細を実装する役割があります。 今回はdomain層のrepositoryにapplication層がfirestoreを使用できるようにinterfaceを用意しているため firestoreのアクセスに関する処理をinfrastructure層に実装しました。

firestoreについて

まずfirestoreとはGoogleが提供しているNoSQL型のcloud databaseです。

firebase.google.com

詳細は上記ドキュメントに書かれてますので割愛しますが、 公式からGolang用のfirestoreのためのSDKが公開されていますし、公式DocsにもGolangでの使用例(他言語も対応)が記載されています。

pkg.go.dev

github.com

firebase.google.com

firestoreを使用するための準備

  1. まずFirebase コンソールにアクセス
  2. "設定" > "サービス アカウント" を開く
  3. "新しい秘密鍵の生成"をクリックし、"キーを生成"をクリックして確定
  4. キーを含む JSON ファイルをproject rootでもどこでもいいので保存 (大切に保管) gitなどpublicなところに後悔しないように.gitignoreの設定を忘れずに!

.envファイルを用意します。筆者の場合はlocal, dev, prodで環境変数を分けることを意識したので.env.localにしています。 creadential情報を持つJSONファイルと同じ階層に.envをセットしてください。

.envの中身

GOOGLE_APPLICATION_CREDENTIALS=./<your_file_name>.json

GOOGLE_APPLICATION_CREDENTIALSという環境変数名は必ず同じにしてください。 というのも、firestoreを初期化は

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/infrastructure/persistence/firestore.go#L25

// .envのGOOGLE_APPLICATION_CREDENTIALSから暗黙的に設定を読み取る
app, err := firebase.NewApp(ctx, nil)

のようなコードでおこなわれるのですが、このとき実行環境に環境変数GOOGLE_APPLICATION_CREDENTIALSが設定されている場合は NewApp時にファイルのパスを指定しなくても暗黙的にロードしてくれるという動きのためです。

これでGolangからfirestoreにアクセスするために必要なリソースは揃いました。 詳細な実装はgithub repositoryを追っていただければと思います。

全ユーザを取得するクエリ

下記のように全ユーザを取得するための実装しました。 この実装でやりたいこととしては、 UserModelにUid(ドキュメントを識別するユニークなID)とデータの中身(名前と都道府県)をセットして、[]UserModel型のリストをapplication層に返してあげることです。(https://github.com/MoriTomo7315/go-user-rest-api/blob/master/domain/model/user.go

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/infrastructure/persistence/firestore.go#L39-L75

// firestoreから全ユーザの情報を取得する
func (f *firestoreClient) GetUsers() (users []*model.User, err error) {
    log.Printf("INFO [GetUsers] connecting firestore start.")

    // ① init firestore client
    ctx := context.Background()
    client, err := initFireStoreClient(ctx)
    defer client.Close()

    if err != nil {
        log.Printf("ERROR firestore clientの初期化に失敗 err=%v", err)
        return nil, define.SYSTEM_ERR
    }

        // ② "users"コレクションからすべてのドキュメントを取得するIteratorを取得
    iter := client.Collection("users").Documents(ctx)
    for {
                // ③ 各ドキュメントをUserModelにマッピングし、usersリストに詰める
        userDocSnap, err := iter.Next()
        if err == iterator.Done {
            break
        }
        if err != nil {
            log.Printf("ERROR firestoreからusersコレクションの検索に失敗 err=%v", err)
            return nil, define.NOT_FOUND_USER
        }
        // Uidをmap[string]interface{}に含める
        userData := userDocSnap.Data()
        userData["id"] = userDocSnap.Ref.ID
        // map[string]interface{} →json []byte -> *model.BookingModel
        jsonuserData, _ := json.Marshal(userData)
        var user *model.User
        json.Unmarshal(jsonuserData, &user)
        users = append(users, user)
    }

    log.Printf("INFO [GetUsers] connecting firestore end.")
    return users, nil
}

firestoreへのアクセス自体はSDKを使用しているので簡単ですが、 戸惑うポイントが返り値です。 この返り値についてはfirestore packageのドキュメントを参照すれば簡単に解決できます。

iter := client.Collection("users").Documents(ctx)の部分で実際にUsersコレクション(RDBでいうところのテーブル)からUserドキュメント(RDBでいうところのレコード)を取得しますが、

iterという返り値はDocumentIterator型であり、複数のUserドキュメントを保持しているものと考えます。

次にiter.Next()で1つずつDocumentSnapshot型のuserDocSnaperror型のerrを受け取っています。 DocumentSnapshot型にはRefフィールドから参照できるDocumentRef型とデータ参照のためのData()関数が存在するのでこれらを活用させていただきます。

ここで注意が必要なのはDocumentSnapshot.Data()の返り値がmap[string]interface{}型ということです。 今回のAPIの例だと、返り値は{"name": "テスト名前", "prefecture": "東京"}のようになるのですが、"テスト名前"や"東京"に当たる部分がどのような形で返ってくるかわからないためstring型ではなくinterface型で返ってくるのです。 そのため例えば、名前だけ参照したい場合には

name string = DocumentSnapshot.Data()["name"].(string)

といったように.(string)で明示的に文字列型に直してあげないといけないです。

でも今回の場合はまとめてmap[string]interface{}型をUserModel型に変換するのでencoding/jsonのMarshalを使用すればUserModel型への構造体に簡単にマッピングすることができます。

interface型について学びたい方はこちらへどうぞ。

go.dev

userIdを指定して特定のユーザを取得するクエリ

下記のようにuserId(UserドキュメントのUid)を指定してユーザを取得するための実装しました。 先ほどの全ユーザを取得する時とは異なり、Uidをつかってfirestoreから取得するドキュメントは一意に定まっています。 なので.Doc(id).Get(ctx)といった感じにfirestoreのSDKを使用します。 この場合の返り値は先ほども紹介したDocumentSnap型なので、同様にDocumentSnap.Ref.IDDocumentSnap.Data()を用いてUserModelに必要なデータを取得します。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/infrastructure/persistence/firestore.go#L77-L107

// firestoreからユーザの情報を取得する
func (f *firestoreClient) GetUserById(id string) (user *model.User, err error) {
    log.Printf("INFO [GetUserById] connecting firestore start. id=%s", id)

    // init firestore client
    ctx := context.Background()
    client, err := initFireStoreClient(ctx)
    defer client.Close()

    if err != nil {
        log.Printf("ERROR firestore clientの初期化に失敗 err=%v", err)
        return nil, define.SYSTEM_ERR
    }


        // Uidを指定してUserドキュメントを取得
    userDocSnap, err := client.Collection("users").Doc(id).Get(ctx)
    if err != nil {
        log.Printf("ERROR firestoreからusersコレクションの検索に失敗 id=%s, err=%v", id, err)
        return nil, define.NOT_FOUND_USER
    }

    // userドキュメントの中身を返却
    // Uidをmap[string]interface{}に含める
    userData := userDocSnap.Data()
    userData["id"] = userDocSnap.Ref.ID
    // map[string]interface{} →json []byte -> *model.BookingModel
    jsonuserData, _ := json.Marshal(userData)
    json.Unmarshal(jsonuserData, &user)

    log.Printf("INFO [GetUserById] connecting firestore end. id=%s", id)
    return user, nil
}

ユーザをfirestoreに登録するクエリ

ユーザ作成する場合はfirestoreのusersコレクションにドキュメントとして登録します。 firestoreにドキュメントとして登録する際はAddメソッドを使用します。引数には map[string]interface{}型のデータを与えます。今回はユーザを作成したいのでname, prefecture, createdAt, updateAtを与えます。 createdAtは作成日を意味するので現在のtimestampをtime.Now()で指定します。updatedAtは更新された日を意味しますが作成のため、nilにしておきます。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/infrastructure/persistence/firestore.go#L109-L137

// firestoreにユーザの情報を作成する
func (f *firestoreClient) CreateUser(user *model.User) (err error) {
    log.Printf("INFO [CreateUser] connecting firestore start. name=%v", user)

    // init firestore client
    ctx := context.Background()
    client, err := initFireStoreClient(ctx)
    defer client.Close()

    if err != nil {
        log.Printf("ERROR firestore clientの初期化に失敗 err=%v", err)
        return define.SYSTEM_ERR
    }

    _, _, err = client.Collection("users").Add(ctx, map[string]interface{}{
        "name":       user.Name,
        "prefecture": user.Prefecture,
        "createdAt":  time.Now(),
        "updatedAt":    nil,
    })
    if err != nil {
        log.Printf("ERROR firestoreのusersコレクションの作成に失敗 err=%v", err)
        return define.FAILED_CREATE_USER
    }

    log.Printf("INFO [CreateUser] connecting firestore end.")
    return nil
}

userIdを指定してユーザ情報を更新するクエリ

ユーザを更新する場合は、firestoreのusersコレクションのドキュメントを更新します。 このときDoc(uid).Setメソッドを使用します。引数にはAddメソッドと同様にmap[string]interface{}を用います。 application層からこのUpdateUserが呼び出される時に更新後のデータを持つ*model.User型のuserを受け取っているので、 それを用いてmap[string]interface{}をSetメソッドに与えます。 このとき、updatedAtの値はtime.Now()にして更新の日時を与えるようにしています。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/infrastructure/persistence/firestore.go#L138-L166

// firestoreのユーザ情報を更新する
func (f *firestoreClient) UpdateUser(user *model.User) (err error) {
    log.Printf("INFO [UpdateUser] connecting firestore start. user=%v", user)

    // init firestore client
    ctx := context.Background()
    client, err := initFireStoreClient(ctx)
    defer client.Close()

    if err != nil {
        log.Printf("ERROR firestore clientの初期化に失敗 err=%v", err)
        return define.SYSTEM_ERR
    }

    _, err = client.Collection("users").Doc(user.Id).Set(ctx, map[string]interface{}{
        "name":       user.Name,
        "prefecture": user.Prefecture,
        "createdAt":  user.CreatedAt,
        "updatedAt":  time.Now(),
    })

    if err != nil {
        log.Printf("ERROR firestoreのusersコレクションの更新に失敗 id=%s, err=%v", user.Id, err)
        return define.FAILED_UPDATE_USER
    }

    log.Printf("INFO [UpdateUser] connecting firestore end.")
    return nil
}

userIdを指定してユーザ情報を削除するクエリ

ユーザ削除の場合、firestore上のuserコレクションのドキュメントも削除する。 ドキュメント削除にはDoc(uid).Deleteメソッドを使用する。特に難しくはないはず。

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/infrastructure/persistence/firestore.go#L168-L191

// firestoreのユーザ情報を削除する
func (f *firestoreClient) DeleteUser(id string) (err error) {
    log.Printf("INFO [DeleteUser] connecting firestore start. userId=%v", id)

    // init firestore client
    ctx := context.Background()
    client, err := initFireStoreClient(ctx)
    defer client.Close()

    if err != nil {
        log.Printf("ERROR firestore clientの初期化に失敗 err=%v", err)
        return define.SYSTEM_ERR
    }

    _, err = client.Collection("users").Doc(id).Delete(ctx)

    if err != nil {
        log.Printf("ERROR firestoreのusersコレクションの削除に失敗 id=%s, err=%v", id, err)
        return define.FAILED_DELETE_USER
    }

    log.Printf("INFO [DeleteUser] connecting firestore end.")
    return nil
}

6. API サーバ起動と動作確認

すこし説明を抜粋しすぎましたが詳細はgitを参照していただければと思います。 本節では作成したAPIを動かしてみます。

main.goにこれまで述べてきたcontroller層、application層、infrastructure層の依存を作成し、portを指定してリクエストを受け付けるサーバを実装します。

main.go

https://github.com/MoriTomo7315/go-user-rest-api/blob/master/main.go

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/MoriTomo7315/go-user-rest-api/application"
    "github.com/MoriTomo7315/go-user-rest-api/controller"
    "github.com/MoriTomo7315/go-user-rest-api/infrastructure/persistence"
    "github.com/joho/godotenv"
)

func main() {
    //log設定
    log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Llongfile)

    // envファイル読み込み
    _ = godotenv.Load(fmt.Sprintf("./.env.%s", os.Getenv("GO_ENV")))

    // Start listening port
    server := http.Server{
        Addr: ":50001",
    }

    /*
       DDD依存関係を定義
       Infrastructure → Application → Controller
   */
    // firestore用infrastructure
    firestoreClient := persistence.NewFirestoreClient()
    userApplication := application.NewUserApplication(firestoreClient)
    userController := controller.NewUserController(userApplication)
    //サーバーにController(ハンドラ)を登録
    log.Printf("/api/users   start")

    http.HandleFunc("/api/users", userController.HandlerHttpRequest)
    http.HandleFunc("/api/users/", userController.HandlerHttpRequestWithParameter)

    server.ListenAndServe()
}

API サーバ起動コマンド

$ cd <your_path>/go-user-rest-api
$ GO_ENV=local go run main.go
2022/02/10 21:20:53.080729 /Users/moritomokana/development/go_study/go-user-rest-api/main.go:36: /api/users   start

7. 動作確認

今回はport 50001で動かしているのでhttp://localhost:50001に対して各endpointの動作確認をします。

GET /api/users

$ curl localhost:50001/api/users | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   667  100   667    0     0    973      0 --:--:-- --:--:-- --:--:--   973
{
  "status": 200,
  "message": "ユーザ情報取得に成功しました。",
  "user_count": 2,
  "users": [
    {
      "id": "CRe4XPnE8DjD4bjk7n2Y",
      "name": "testuser6",
      "prefecture": "神奈川県",
      "createdAt": "2022-01-29T13:52:38.936004Z",
      "updatedAt": ""
    },
    {
      "id": "oaAwBZMSdmEC4OEbr99B",
      "name": "testuser5",
      "prefecture": "東京都",
      "createdAt": "2022-01-29T12:00:00Z",
      "updatedAt": "2022-01-29T13:50:59.146083Z"
    }
  ]
}
~

GET /api/users/:user_id

$ curl localhost:50001/api/users/oaAwBZMSdmEC4OEbr99B | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   248  100   248    0     0    224      0  0:00:01  0:00:01 --:--:--   224
{
  "status": 200,
  "message": "ユーザ情報取得に成功しました。",
  "user_count": 1,
  "users": [
    {
      "id": "oaAwBZMSdmEC4OEbr99B",
      "name": "testuser5",
      "prefecture": "東京都",
      "createdAt": "2022-01-29T12:00:00Z",
      "updatedAt": "2022-01-29T13:50:59.146083Z"
    }
  ]
}

POST /api/users

$ curl -X POST -H "Content-Type: application/json" -d '{"name":"testuser7", "prefecture":"大阪府"}' localhost:50001/api/users | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   131  100    85  100    46     38     20  0:00:02  0:00:02 --:--:--    59
{
  "status": 201,
  "message": "作成に成功しました。",
  "user_count": 0,
  "users": null
}

念の為、testuser7が作成されているか確認。

$ curl localhost:50001/api/users | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   798  100   798    0     0   3179      0 --:--:-- --:--:-- --:--:--  3179
{
  "status": 200,
  "message": "ユーザ情報取得に成功しました。",
  "user_count": 3,
  "users": [
    {
      "id": "CRe4XPnE8DjD4bjk7n2Y",
      "name": "testuser6",
      "prefecture": "神奈川県",
      "createdAt": "2022-01-29T13:52:38.936004Z",
      "updatedAt": ""
    },
    {
      "id": "oaAwBZMSdmEC4OEbr99B",
      "name": "testuser5",
      "prefecture": "東京都",
      "createdAt": "2022-01-29T12:00:00Z",
      "updatedAt": "2022-01-29T13:50:59.146083Z"
    },
    {
      "id": "uCcJCLGDxwmpmjIfZdUk",
      "name": "testuser7",
      "prefecture": "大阪府",
      "createdAt": "2022-02-10T12:36:27.945981Z",
      "updatedAt": ""
    }
  ]
}

POST /api/users/:user_id

testuser7のprefectureを大阪府から京都府に変更してみる。

curl -X POST -H "Content-Type: application/json" -d '{"name": "testuser7", "prefecture":"京都府", "createdAt":"2022-02-10T12:36:27.945981Z"}' localhost:50001/api/users/uCcJCLGDxwmpmjIfZdUk | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   175  100    85  100    90     38     40  0:00:02  0:00:02 --:--:--    79
{
  "status": 204,
  "message": "更新に成功しました。",
  "user_count": 0,
  "users": null
}

更新されたかどうか確認。

$ curl localhost:50001/api/users/uCcJCLGDxwmpmjIfZdUk | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   254  100   254    0     0    261      0 --:--:-- --:--:-- --:--:--   261
{
  "status": 200,
  "message": "ユーザ情報取得に成功しました。",
  "user_count": 1,
  "users": [
    {
      "id": "uCcJCLGDxwmpmjIfZdUk",
      "name": "testuser7",
      "prefecture": "京都府",
      "createdAt": "2022-02-10T12:36:27.945981Z",
      "updatedAt": "2022-02-10T12:41:18.51411Z"
    }
  ]
}

DELETE /api/users/:user_id

testuser7 (uid: uCcJCLGDxwmpmjIfZdUk)を削除してみる。

$ curl -X DELETE localhost:50001/api/users/uCcJCLGDxwmpmjIfZdUk | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    85  100    85    0     0     33      0  0:00:02  0:00:02 --:--:--    33
{
  "status": 204,
  "message": "削除に成功しました。",
  "user_count": 0,
  "users": null
}

testuser7が消えた確認。

$ curl localhost:50001/api/users | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   667  100   667    0     0    699      0 --:--:-- --:--:-- --:--:--   699
{
  "status": 200,
  "message": "ユーザ情報取得に成功しました。",
  "user_count": 2,
  "users": [
    {
      "id": "CRe4XPnE8DjD4bjk7n2Y",
      "name": "testuser6",
      "prefecture": "神奈川県",
      "createdAt": "2022-01-29T13:52:38.936004Z",
      "updatedAt": ""
    },
    {
      "id": "oaAwBZMSdmEC4OEbr99B",
      "name": "testuser5",
      "prefecture": "東京都",
      "createdAt": "2022-01-29T12:00:00Z",
      "updatedAt": "2022-01-29T13:50:59.146083Z"
    }
  ]
}

8. まとめ

今後の課題

  • User更新は画面からリクエストされる想定なので、全項目値を受け取るように簡単に実装したが、変更点のみのものを受け取るように実装してもいいかもしれない。
  • エラーハンドリングをもっと学ぶ
  • Testちゃんと書く
  • Cloud Runなどにデプロイしてみる。