Lean Baseball

No Engineering, No Baseball.

Cloud Run上のGo製RESTful APIからBigQueryとCloud Storageを使う - PythonからGoに変えた理由を添えて

仕事がずっとコンサルワークなので, 休日のプログラミングがめちゃくちゃ楽しみになっている人です.

最初にお礼をさせてください, Developers Summit 2023の発表, なんだか好評価(高評価)だったみたいです.

練習やレビューに協力いただいた皆様, そして何よりも当日私のトークを聞いていただいた皆様, 誠にありがとうございました.

さて, 当日の発表では「GoでRESTful APIを作って運用しましたよ」という話をしました.

ただ, この資料のスライド枚数(約70枚)中, わずか1スライドでサラッと触れた程度で何も話をしていないので,

  • Cloud RunでGo製アプリを動かす
  • GoからBigQuery, Cloud Storageを使う
  • Pythonじゃなくて敢えてGoにした理由

について, このエントリーで改めて書きたいと思います.

この話の範囲・ユースケースはこちらで,

バックエンドをどうやって作った話です

「BigQueryでSelectした結果をRESTful APIで返す」「検索結果のCacheをCloud Storageで持つ」という構成です*1.

また, 調べながら作って苦労してる時の呟きも残ってるのでこちらも合わせて読むといいかもしれません, 私と同じく駆け出しGopherさんは.

そう, ちゃんと書いたの7年ぶりだったんですよ, ほとんど忘れてました...w

なお本日のスタメン(メニュー)はこちらです.

対象読者と前提条件

  • Go言語で何かしらのものを作ったり書いたりしている方. なお執筆者(私)のGo歴は割と浅いですw*2
  • 何かしらの言語でCloud Runを使ったことがある方. 無い方はクイックスタートをやってみることをオススメします.
  • サーバーサイドのAPI開発, 運用をしている・経験ある方.

Cloud Run, BigQuery, Cloud Storageといったサービスの解説は省略します, それぞれのページをご覧頂くか, 私のデブサミ発表を御覧ください.

前提条件として, GoのAPIは以下条件の元実装しています.

  • Goのバージョンは 1.20.1
  • Web FrameworkとしてGinを使用

コードスニペットが多数登場しますが, コピペでは動かない奴ですご了承ください🙏

「Ginじゃなくて標準のnet/httpで良くね?」とかツッコミたくなる方もいらっしゃると思いますが, まあ今回はFrameworkが何であっても関係ない話なので許してください*3.

また, 「PythonよりGoがいいぜ」という話ではないことを強く言っておきます.

理由は最後にちゃんと書きますが, 単に使い分けと好みの話ということで読んでもらえれば.

Cloud RunでGo製 APIを作る

Cloud RunでGoを動かすのはさほど難しくありません, クイックスタートを写経したらひとまずHello Worldは余裕です.

cloud.google.com

改めてここに一つずつ書くのは無駄な繰り返しな気がするので割愛します.

「クイックスタートを終えたあとに何をするか」という話からします.

ひとまず作る・動かす

ひとまず作って動かすだけなら,

  1. クイックスタート通りに写経して一旦デプロイする
  2. 必要なライブラリ(今回はGin)をgo install github.com/gin-gonic/gin とかやって入手する
  3. 必要なコードを書いてデプロイ -> 動くかどうか確認
  4. Dockerfileを書いて手元で動かす
  5. うまく行ったらビルドしてArtifact Registryにimageをpush*4
  6. docker imageを元にCloud Runにデプロイ

ぐらいで一旦完了です.

私も一旦このような手順で作りました, コードの雰囲気だけ伝えるとこんな感じです.

// main.goの内容
package main

import (
    "github.com/Shinichi-Nakagawa/sample/api/fielding"
    "github.com/Shinichi-Nakagawa/sample/api/tracking"
    "github.com/gin-gonic/gin"
)

func getTrackingByBatter(c *gin.Context) {
    var response tracking.Response
    // 何かしらの処理(省略)
    c.IndentedJSON(http.StatusOK, response)
}

func getTrackingByPitcher(c *gin.Context) {
    var response tracking.Response
    // 何かしらの処理(省略)
    c.IndentedJSON(http.StatusOK, response)
}

func getFielding(c *gin.Context) {
    var response fielding.Response
    // 何かしらの処理(省略)
    c.IndentedJSON(http.StatusOK, response)
}

func main() {
    router := gin.Default()
    router.GET("/tracking/batter/:name", getTrackingByBatter)
    router.GET("/tracking/pitcher/:name", getTrackingByPitcher)
    router.GET("/fielding/:name", getFielding)
    router.Run(":8080")
}

依存する外部ライブラリなんかもあるので, Dockerfileも書きました.

# Dockerfileの内容

# Build Stage
FROM golang:1.20 as  builder


# /app以下に必要なもの全部ある前提
WORKDIR /app
COPY . /app/
RUN go mod download

RUN CGO_ENABLED=0 go build -o /go/bin/app


# Run Stage(Build Stageからバイナリをコピっているだけ)
FROM alpine:latest

COPY --from=builder go/bin/app /

EXPOSE 8080

CMD [ "/app" ]

これで手元で docker compose up とかで動いたらOK.

gcloud build した結果をArtifact Registryに保存するためいろんな設定を書きます.

以下の内容でcloudbuild.yaml を記載.

steps:
  - name: "gcr.io/cloud-builders/docker"
    args:
      [
        "build",
        "-t",
        "asia-northeast1-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/${_IMAGE}:${_TAG}",
        ".",
      ]
images:
  - "asia-northeast1-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/${_IMAGE}:${_TAG}"

こんな感じでbuild -> deployのシェルを書いて動かせるようにします(deploy.sh という名前で保存*5).

project=$1
tag=$2
# image build & submit
gcloud builds submit --config=cloudbuild.yaml \
  --substitutions=_REPOSITORY=sample,_IMAGE=api,_TAG=${tag} .

# deploy
gcloud run deploy --image asia-northeast1-docker.pkg.dev/${project}/sample/api:${tag} \
 --platform managed \
 --port 8080 \
 --memory 4Gi \
 --cpu 2 \
 --region asia-northeast1 \
 --max-instances 4 \
 --min-instances 0

ここまで来たら,

sh ./deploy.sh ${プロジェクトの名前} ${docker imageのタグ名}

とかでひとまず一通りのモノが動きます.

GitHub ActionsでCI/CD

いつまでも手動でデプロイするのはイケていないので,

  • リポジトリ(GitHub)のブランチ(何でもいい)にpushしたらテストが走る.
  • mainブランチだったらCloud Buildが走ってからのCloud Runにデプロイ.

というCI/CDをGitHub Actionsに加えます.

絵で描くとこういうやつです.

.github/workflows/api.yaml という名前で↑のCI/CDが実現できます(ちなみにapi.yamlのファイル名は何でもいいです).

name: API
on: push
defaults:
  run:
    working-directory: api  # work directoryを固定
env:
  PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}  # Google CloudのプロジェクトID
  REPOSITORY: sample
  SERVICE_NAME: api
  REGION: asia-northeast1
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Go
        uses: actions/setup-go@v3
        with:
          go-version: "1.20"
      - name: Build
        run: go build -v ./...
      - name: Test
        run: go test -v ./...
  build:
    name: Build
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    permissions:
      contents: "read"
      id-token: "write"
    steps:
      - id: "checkout"
        name: "Checkout"
        uses: actions/checkout@v3
      - id: "auth"  # Workload Identity(WID)を使った認証認可
        name: "Authenticate to Google Cloud"
        uses: "google-github-actions/auth@v0"
        with:
          token_format: "access_token"
          workload_identity_provider: "${{ secrets.GCP_WID_PROVIDER }}"
          service_account: "${{ secrets.GCP_SERVICE_ACCOUNT }}"
      - id: "docker-auth"  # Artifact Registryにログイン(WIDで取ったtokenを使う)
        name: Authorize Docker
        uses: "docker/login-action@v1"
        with:
          username: "oauth2accesstoken"
          password: "${{ steps.auth.outputs.access_token }}"
          registry: "${{ env.REGION }}-docker.pkg.dev"
      - id: "docker-build"
        name: Build Docker image
        run: docker build -t "${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE_NAME }}:${{ github.sha }}" .
      - id: "docker-push"
        name: Push Docker Image
        run: docker push "${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE_NAME }}:${{ github.sha }}"
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    permissions:
      contents: "read"
      id-token: "write"
    env:
      GIN_MODE: "release"  # Ginをリリースモードで動かす
    steps:
      - id: "checkout"
        name: "Checkout"
        uses: actions/checkout@v3
      - id: "auth"  # Workload Identity(WID)を使った認証認可(Build Stageと同じことをしています)
        name: "Authenticate to Google Cloud"
        uses: "google-github-actions/auth@v0"
        with:
          workload_identity_provider: "${{ secrets.GCP_WID_PROVIDER }}"
          service_account: "${{ secrets.GCP_SERVICE_ACCOUNT }}"
      - id: "deploy"
        name: "Deploy to Cloud Run"
        uses: "google-github-actions/deploy-cloudrun@v0"
        with:
          service: "${{ env.SERVICE_NAME }}"
          region: "${{ env.REGION }}"
          image: "${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE_NAME }}:${{ github.sha }}"
          env_vars: "ENV_HOGE=${{ secrets.ENV_HOGE }}"  # Container内で使う環境変数
          flags: "--cpu=2 --memory=4Gi --min-instances=0 --max-instances=4 --port=8080"
      - id: "output"
        name: Show Output
        run: echo ${{ steps.deploy.outputs.url }}

ここまで組み込んだら,

  • 毎push毎にビルドとテストが走る
  • mainブランチにpushかつテストがすべて通るとbuild -> deployされてCloud Runのアプリケーションが入れ替わる

というCI/CDが爆誕します.

mainブランチにpushして成功した時の状態

GoからBigQueryを使う

GoからBigQueryを使いたいときはbigqueryパッケージを使います.

pkg.go.dev

cloud.google.com

アプリケーションに組み込む

使い方はドキュメントの通りなので,

  • main.go とは別のファイルにBigQueryクライアントを実装
  • main.go にクライアントを取り込んで使う

こんな感じで組み込みます.

// datastore/bq.go

package datastore

import (
    "cloud.google.com/go/bigquery"
    "context"
    "log"
)

func NewBigQueryClient(ctx context.Context, projectID string) *bigquery.Client {
    client, err := bigquery.NewClient(ctx, projectID)
    if err != nil {
        log.Fatalf("bigquery.NewClient: %v", err)
    }
    defer client.Close()
    return client
}

main.go への組み込みはこんな感じ(もっといい書き方あったら教えて下さい).

package main

import (
    "context"
    "fmt"
    "cloud.google.com/go/bigquery"
    "github.com/Shinichi-Nakagawa/sample/api/datastore"
    "github.com/Shinichi-Nakagawa/sample/api/fielding"
    "github.com/Shinichi-Nakagawa/sample/api/tracking"
    "github.com/gin-gonic/gin"
)

var ctx context.Context = context.Background()
// BigQuery Client
var bq *bigquery.Client


func getTrackingByBatter(c *gin.Context) {
    var response tracking.Response
    // 何かしらの前処理(省略)
    var results []tracking.SelectResult
         queryString := fmt.Sprintf(`select a, b, c from hoge.fielding where d = 1`)  //  適当なクエリ文字列
    query := bq.Query(queryString)
    iter, err := query.Read(ctx)
    if err != nil {
        log.Fatalf("query.Read: %v", err)
    }
    for {
        var row tracking.SelectResult
        err := iter.Next(&row)
        if err == iterator.Done {
            return results
        }
        if err != nil {
            log.Fatalf("iter.Next: %v", err)
        }
        results = append(results, row)
    }
         //  resultsからいい感じにresponseを作る処理(省略)
    c.IndentedJSON(http.StatusOK, response)
}

func getTrackingByPitcher(c *gin.Context) {
    var response tracking.Response
    // 何かしらの前処理(省略)
    var results []tracking.SelectResult
         queryString := fmt.Sprintf(`select a, b, c from hoge.fielding where d = 1`)  //  適当なクエリ文字列
    query := bq.Query(queryString)
    iter, err := query.Read(ctx)
    if err != nil {
        log.Fatalf("query.Read: %v", err)
    }
    for {
        var row tracking.SelectResult
        err := iter.Next(&row)
        if err == iterator.Done {
            return results
        }
        if err != nil {
            log.Fatalf("iter.Next: %v", err)
        }
        results = append(results, row)
    }
         //  resultsからいい感じにresponseを作る処理(省略)
    c.IndentedJSON(http.StatusOK, response)
}

func getFielding(c *gin.Context) {
    var response fielding.Response
    // 何かしらの前処理(省略)
    var results []fielding.SelectResult
         queryString := fmt.Sprintf(`select a, b, c from hoge.fielding where d = 1`)  //  適当なクエリ文字列
    query := bq.Query(queryString)
    iter, err := query.Read(ctx)
    if err != nil {
        log.Fatalf("query.Read: %v", err)
    }
    for {
        var row fielding.SelectResult
        err := iter.Next(&row)
        if err == iterator.Done {
            return results
        }
        if err != nil {
            log.Fatalf("iter.Next: %v", err)
        }
        results = append(results, row)
    }
         //  resultsからいい感じにresponseを作る処理(省略)
    c.IndentedJSON(http.StatusOK, response)
}

func main() {
    // Initialize 
    bq = datastore.NewBigQueryClient(ctx, googleCloudProjectID)
    router := gin.Default()
    router.GET("/tracking/batter/:name", getTrackingByBatter)
    router.GET("/tracking/pitcher/:name", getTrackingByPitcher)
    router.GET("/fielding/:name", getFielding)
    router.Run(":8080")
}


こんな感じで書いて程よく動きました.

データ型は組み込み型を使う

Schema的な使い方をするstructはbigqueryパッケージの組み込み型を使いましょう(でないとNull対応ができない).

package fielding

import (
    "cloud.google.com/go/bigquery"
)

// jsonやcsvと相互で読み込み・書き込みするのでこんな感じになっています.
type SelectResult struct {
    GameDate                 bigquery.NullDate    `bigquery:"game_date" json:"game_date" csv:"game_date"`
    Inning                   bigquery.NullInt64   `bigquery:"inning" json:"inning" csv:"inning"`
    FielderName              bigquery.NullString  `bigquery:"fielder_name" json:"fielder_name" csv:"fielder_name"`
    FielderPosition          bigquery.NullInt64   `bigquery:"fielder_position" json:"fielder_position" csv:"fielder_position"`
    PitcherName              bigquery.NullString  `bigquery:"pitcher_name" json:"pitcher_name" csv:"pitcher_name"`
    HitLocation              bigquery.NullInt64   `bigquery:"hit_location" json:"hit_location" csv:"hit_location"`
    // 以下, 100個近くあるので省略
    LaunchSpeedAngleCategory bigquery.NullString  `bigquery:"launch_speed_angle_category" json:"launch_speed_angle_category" csv:"launch_speed_angle_category"`
    OfFieldingAlignment      bigquery.NullString  `bigquery:"of_fielding_alignment" json:"of_fielding_alignment" csv:"of_fielding_alignment"`
    IfFieldingAlignment      bigquery.NullString  `bigquery:"if_fielding_alignment" json:"if_fielding_alig`
}

値に欠損がある可能性が高いデータ型はNull** 的なデータ型を使うのがポイントです, パッケージのリファレンスにも記載があるので見てみると良いでしょう.

requiredなカラムであればNull的なのは不要です.

GoからCloud Storageを使う

GoからBigQueryを使いたいときはstorageパッケージを使います.

pkg.go.dev

cloud.google.com

使い方はドキュメントの通りかつ, 雰囲気的にはbigqueryとさほど変わりません.

  • main.go とは別のファイルにCloud Storageクライアントを実装
  • main.go にクライアントを取り込んで使う

BigQueryと同じ感じで組み込みます.

// datastore/gcs.go
package datastore

import (
    "cloud.google.com/go/storage"
    "context"
    "fmt"
    "io"
    "log"
)


// クライアントの初期化
func NewStorageClient(ctx context.Context) *storage.Client {
    client, err := storage.NewClient(ctx)
    if err != nil {
        log.Fatalf("storage.NewClient: %v", err)
    }
    return client
}


// バケット取得
func GetBucket(client *storage.Client, name string) *storage.BucketHandle {
    bucket := client.Bucket(name)
    return bucket

}

// オブジェクトの書き込み(文字列をファイルに書き込む)
func WriteObject(bkt *storage.BucketHandle, name string, value string, ctx context.Context) {
    obj := bkt.Object(name)
    w := obj.NewWriter(ctx)
    if _, err := fmt.Fprintf(w, value); err != nil {
        log.Fatalf("fmt.Fprintf: %v", err)
    }
    if err := w.Close(); err != nil {
        log.Fatalf("w.Close: %v", err)
    }
}

// オブジェクトの読み込み(テキストファイルを読み込んで文字列にする)
func ReadObject(bkt *storage.BucketHandle, name string, ctx context.Context) string {
    var result string
    obj := bkt.Object(name)
    r, err := obj.NewReader(ctx)
    if err != nil {
        if err.Error() == "storage: object doesn't exist" {
            return result
        }
        log.Fatalf("obj.NewReader: %v", err)
    }
    defer r.Close()
    b, err := io.ReadAll(r)
    if err != nil {
        log.Fatalf("io.ReadAll: %v", err)
    }
    result = string(b)
    return result
}

私が作ったAPIでは,

  • 関数呼び出しのCacheとしてヒットしたらファイルの中身を読み込み
  • ヒットしないときは新規にファイルを書き込み

というCache的な使い方*6をしているので, main.goでこんな感じで書き込みます.

package main

import (
    "context"
    "fmt"
    "encoding/json"
    "cloud.google.com/go/bigquery"
    "cloud.google.com/go/storage"
    "github.com/Shinichi-Nakagawa/sample/api/datastore"
    "github.com/Shinichi-Nakagawa/sample/api/fielding"
    "github.com/Shinichi-Nakagawa/sample/api/tracking"
    "github.com/gin-gonic/gin"
)

var ctx context.Context = context.Background()
// BigQuery Client
var bq *bigquery.Client
// Cloud Storage Bucket
var bucket *storage.BucketHandle


func getTrackingByBatter(c *gin.Context) {
    var response tracking.Response
    // 何かしらの前処理(省略)
    // ファイル名を生成してひとまずRead
    filename := fmt.Sprintf("hoge/fuga.json")
    responseCache := datastore.ReadObject(bucket, filename, ctx)
    // 中身があったらJSONに読み直して戻す
    if responseCache != "" {
        if err := json.Unmarshal([]byte(responseCache), &response); err != nil {
            c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid Response"})
            return
        }
        c.IndentedJSON(http.StatusOK, response)
        return
    }
    var results []tracking.SelectResult
         queryString := fmt.Sprintf(`select a, b, c from hoge.fielding where d = 1`)  //  適当なクエリ文字列
    query := bq.Query(queryString)
    iter, err := query.Read(ctx)
    if err != nil {
        log.Fatalf("query.Read: %v", err)
    }
    for {
        var row tracking.SelectResult
        err := iter.Next(&row)
        if err == iterator.Done {
            return results
        }
        if err != nil {
            log.Fatalf("iter.Next: %v", err)
        }
        results = append(results, row)
    }
         // resultsからいい感じにresponseを作る処理(省略)
         // Cacheに書き込み
         datastore.WriteObject(bucket, filename, "書き込み対象の文字列", ctx)
    c.IndentedJSON(http.StatusOK, response)
}

func getTrackingByPitcher(c *gin.Context) {
    var response tracking.Response
    // 何かしらの前処理(省略)
    // ファイル名を生成してひとまずRead
    filename := fmt.Sprintf("hoge/fuga.json")
    responseCache := datastore.ReadObject(bucket, filename, ctx)
    // 中身があったらJSONに読み直して戻す
    if responseCache != "" {
        if err := json.Unmarshal([]byte(responseCache), &response); err != nil {
            c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid Response"})
            return
        }
        c.IndentedJSON(http.StatusOK, response)
        return
    }
    var results []tracking.SelectResult
         queryString := fmt.Sprintf(`select a, b, c from hoge.fielding where d = 1`)  //  適当なクエリ文字列
    query := bq.Query(queryString)
    iter, err := query.Read(ctx)
    if err != nil {
        log.Fatalf("query.Read: %v", err)
    }
    for {
        var row tracking.SelectResult
        err := iter.Next(&row)
        if err == iterator.Done {
            return results
        }
        if err != nil {
            log.Fatalf("iter.Next: %v", err)
        }
        results = append(results, row)
    }
         // resultsからいい感じにresponseを作る処理(省略)
         // Cacheに書き込み
         datastore.WriteObject(bucket, filename, "書き込み対象の文字列", ctx)
    c.IndentedJSON(http.StatusOK, response)
}

func getFielding(c *gin.Context) {
    var response fielding.Response
    // 何かしらの前処理(省略)
    // ファイル名を生成してひとまずRead
    filename := fmt.Sprintf("hoge/fuga.json")
    responseCache := datastore.ReadObject(bucket, filename, ctx)
    // 中身があったらJSONに読み直して戻す
    if responseCache != "" {
        if err := json.Unmarshal([]byte(responseCache), &response); err != nil {
            c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid Response"})
            return
        }
        c.IndentedJSON(http.StatusOK, response)
        return
    }
    var results []fielding.SelectResult
         queryString := fmt.Sprintf(`select a, b, c from hoge.fielding where d = 1`)  //  適当なクエリ文字列
    query := bq.Query(queryString)
    iter, err := query.Read(ctx)
    if err != nil {
        log.Fatalf("query.Read: %v", err)
    }
    for {
        var row fielding.SelectResult
        err := iter.Next(&row)
        if err == iterator.Done {
            return results
        }
        if err != nil {
            log.Fatalf("iter.Next: %v", err)
        }
        results = append(results, row)
    }
         // resultsからいい感じにresponseを作る処理(省略)
         // Cacheに書き込み
         datastore.WriteObject(bucket, filename, "書き込み対象の文字列", ctx)
    c.IndentedJSON(http.StatusOK, response)
}

func main() {
    // Initialize 
    bq = datastore.NewBigQueryClient(ctx, googleCloudProjectID)
    gcs := datastore.NewStorageClient(ctx)
    bucket = datastore.GetBucket(gcs, "buket-name")
    router := gin.Default()
    router.GET("/tracking/batter/:name", getTrackingByBatter)
    router.GET("/tracking/pitcher/:name", getTrackingByPitcher)
    router.GET("/fielding/:name", getFielding)
    router.Run(":8080")
}

この辺は割と直感的なので書きやすかったかも.

結び - PythonとGo使い分け

というわけで,

  • Cloud RunでGo製アプリを動かす
  • GoからBigQuery, Cloud Storageを使う

話を(GitHub ActionsからのCI/CDも加えた上で)紹介しました.

なんやかんやでやってることが盛りだくさんで端折った箇所もあるので, 「もう少し詳しく」的なリクエストありましたらSNS等でコメントを頂戴出来ると幸いです.

最後に, Pythonじゃなくて敢えてGoにした理由 についてお話をすると,

  1. 仕様が決まりやすいかつ, どのみち型安全に実装するなら最初からGoとかRustでいいじゃん!(最大の理由)*7
  2. クラウドネイティブ, Containerとの相性を考えるとGoいいじゃん(二番目の理由)
  3. 10年以上使ってるPythonでやれば休みの日丸一日使ったらdeployまで持っていけるが, それだと面白くない(違う言語も使いたい).

私はPythonもJavaScriptも極力, 型安全を担保して作りたい人でtypehint(Python)もTypeScriptも使うのですが,

Backend作る目的でPython書いてる時に型のコードだらけで何と戦っているのかイマイチわからん

というか,

「Goに馴染む学習コストを払ってでも最初から静的型付けな言語で書くべきじゃね?」

となりこのような選択になりました.

また,

  • コンテナ化するためにimage/Containerのサイズを小さくする
  • CI/CD(継続的インテグレーション・継続的デプロイ)をシュッとした構成でやる

という観点で,

Goが持つ言語仕様や特性が「クラウドネイティブな開発」という視点で照らし合わせるとPythonより良いんじゃない?

という知識(と過去経験から来る実感)があったのですが, いかんせん手を動かして試したことがなかったのでやりたかったのも大きな理由の一つです.

&結果的に開発者体験と共に知識も習得できましたし, 思ってたことは大正解でした, 他の言語で苦労してたの何だったんだマジで.

今回のコードは2022年の末から1月にかけて(デブサミに間に合うように)作ったものですが, 良きことも悪きこともたくさん学んだので,

今後はやりたいことの親和性・特徴に合わせてPythonとGoとTypeScriptを渡り歩きたいなと思います.

一個言語と環境知ってればひとまずエンジニアリングできる, なんて時代じゃないんで色々使ってやってくぞ.

最後までお読みいただきありがとうございました.

Appendix: 参考文献

7年ぶりのGolang開発ではこちらの書籍が私の相棒として大活躍してくださりました.

また, 最近始まったSoftware Design詩の連載「なるほど納得Go言語」を読みながら学んだりリファクタリングしたりしています.

*1:後者のCacheはともかく, 前者のBigQueryで検索した結果をAPIに, はあるようで無いケースかつ, あんまり使う機会も無いかもしれません. 状況によってBad Caseだと思うので.

*2:実務で書いたのはほぼゼロに等しいですが, 自分用の何かを作る・運用する程度には理解しています

*3:何だったらオススメのFrameworkとか方法があったら教えて下さい.

*4:この手のやつはググると代替Container Registryが出てくると思いますが, 機能面・セキュリティ面云々の事もあるので後発であるArtifact Registryを使うのがベストです&移行もさほど難しくないと思います(Enterpriseな使い方じゃなければ.)参考

*5:makeファイルじゃなくてすいません...w

*6:Cloud FirestoreとかのBigTable使えよ!とツッコまれそうですが, ドキュメントサイズの限界および, DB系はそれなりに処理とお金のコストがかかるのでこのような仕様にしました.

*7:Rustにしなかったのはこの程度のAPIをRustで作るのは流石にやりすぎと感じたからです.