GoのAPIサーバーでmiddlewareを適用する時にaliceを使って楽をする
GoでAPIサーバを作っている時にmiddlewareを適用することはよくあると思うのですが、数が多いと下のようにゴテゴテした感じになると思います。
func main() { mux := http.NewServeMux() mux.HandleFunc("/handler1", handler1) handler := middle1(middle2(mux)) log.Fatal(http.ListenAndServe(":8080", handler)) }
この例ではmiddlewareが2つだけなんでこれで済んでますが、増えてくるとゴテゴテ度は増大していくと思います。
この部分、なんかいい方法ないか、探していたらやっぱりありました。
早速、試してみました。
インストール
go get github.com/justinas/alice
aliceを使ったサンプル
alice.New(
の部分がそうでこういう風に手軽にmiddlewareを適用できるようになる。
func main() { mux := http.NewServeMux() mux.HandleFunc("/handler1", handler1) handler := alice.New(middle1, middle2).Then(mux) log.Fatal(http.ListenAndServe(":8080", handler)) } func middle1(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Println("middle1 start") next.ServeHTTP(w, r) fmt.Println("middle1 end") }) } func middle2(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Println("middle2 start") next.ServeHTTP(w, r) fmt.Println("middle2 end") }) }
実行結果
上記のサーバを起動して、別ターミナルから以下のようにリクエストを飛ばす。
curl http://localhost:8080/handler1
サーバ側のログに以下のように表示され、無事middlewareが適用されていることが確認できる。 便利。
middle1 start middle2 start middle2 end middle1 end
GoのAPIサーバのレスポンスをgzip圧縮する
今、仕事で作っているGoで作られたAPIサーバは比較的、レスポンスが巨大なものが多く、gzip圧縮した方が良さそうに感じた。
APIサーバはGCP上のAPI gatewayが前段にあり、その後ろでCloud Runで実際の処理が動いている構成。 最初はAPI gatewayとかにgzip圧縮する機能とかありそうとか思ってたが、そういう機能はないみたいなのでAPIサーバ側で実装してみた。
使ったライブラリ
下のリンク先のものを使った。 基本的には1024バイトを超えるレスポンスでないとgzip圧縮しないようになっていたので、小さい大きさのレスポンスの時の圧縮・解凍による性能劣化みたいなことは少ないと思う。
インストール
go get github.com/klauspost/compress
組み込み例
package main import ( "log" "net/http" "github.com/klauspost/compress/gzhttp" ) func main() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte("{\"test\": \"--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\"}")) }) handler := gzhttp.GzipHandler(handler) log.Fatal(http.ListenAndServe(":8080", handler)) }
動作確認
curlで以下のようにして確認。
--compressed
オプションを付けることでAccept-Encoding: gzip
みたいな感じなる模様。
リクエスト
curl -v --compressed http://localhost:8080/
レスポンス
レスポンスヘッダーにContent-Encoding: gzip
が意図どおりに付いているので、gzip圧縮されていることがわかる。
* Trying 127.0.0.1:8080... * Connected to localhost (127.0.0.1) port 8080 (#0) > GET / HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.86.0 > Accept: */* > Accept-Encoding: deflate, gzip, br, zstd > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Content-Encoding: gzip < Content-Type: application/json < Vary: Accept-Encoding < Date: Sun, 10 Dec 2023 11:15:36 GMT < Content-Length: 48 < {"test": "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------* Connection #0 to host localhost left intact ------------"}%
Cloud Run jobsを試してみた
タイトルどおり、Cloud Run jobsを使ったことがなかったので試してみた。
手順
検証用のアプリ作成
main.go
package main import "fmt" func main() { fmt.Println("start process") fmt.Println("something process") fmt.Println("finish process") }
Dockerfile
FROM golang:1.21-alpine AS builder WORKDIR /usr/local/src/ ADD go.mod ./ RUN go mod download ADD . . RUN go build -o /go/bin/app ./main.go FROM alpine:latest COPY --from=builder /go/bin/app / CMD ["/app"]
動作確認
下のように3つの文字列が表示されたら、OK
# 動作確認 > docker build . -t test-app > docker run --rm test-app start process something process finish process
イメージのpush
artifact registoryにレポジトリを作成し、イメージをpushする
> PROJECT=$(gcloud config get-valud project) > REGION=$(gcloud config get-value compute/region) # dockerとの連携(docker pushするために必要) > gcloud auth configure-docker asia-northeast1-docker.pkg.dev # レポジトリの作成 > gcloud artifacts repositories create docker-repo --repository-format=docker --location="${REGION}" --description="Docker repository" # レポジトリのリスト確認 > gcloud artifacts repositories list # tag付けしてpush > REPO="asia-northeast1-docker.pkg.dev/${PROJECT}/docker-repo" > docker tag test-app "${REPO}/test-app" > docker push "${REPO}/test-app"
ジョブを作成し、実行
> gcloud run jobs create sample-job --image "${REPO}/test-app:latest" --region "${REGION}" > gcloud run jobs execute sample-job --region "${REGION}" # 実行結果の確認 > gcloud beta run jobs logs read sample-job --region "${REGION}" 2023-10-15 04:09:19 start process 2023-10-15 04:09:19 something process 2023-10-15 04:09:19 finish process
とりあえず、jobを動作させることはできた。 実際にはCloud Schedulerでの定期実行とは何かのPub/Subあたりでのイベント駆動をさせるのが使い方になるんだとは思う。
後片付け
gcloud run jobs delete sample-job --region "${REGION}" gcloud artifacts repositories delete docker-repo --location "${REGION}"
参考
GCP Cloud RunでDockerfileを作らなくてもデプロイして動作が可能か検証
Cloud Runで何かのアプリをデプロイする時にはコンテナイメージを作ったり、githubと連携したりする必要がある、と誤解していましたが、デプロイコマンドの--sourceオプションを使用することでローカルのソースからデプロイまで可能という記事を読んで、自分でも試してみました。
手順
検証用のアプリ作成
アクセスするとJSONを返却するだけの検証用APIサーバを作成します。
main.go
package main import ( "encoding/json" "log" "net/http" "os" ) type Hello struct { Title string Desc string Ver string } func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(Hello{Title: "Hello", Desc: "BuildPack", Ver: "0.0.1"}) }) port := os.Getenv("PORT") log.Printf("Listening on %v", port) log.Fatal(http.ListenAndServe(":"+port, nil)) }
動作確認
まずはローカルで動作確認します。 ターミナルを開いて上記のコマンドでAPIサーバを起動。
export PORT=8080 go run main.go
別のターミナルを開き、APIサーバにリクエストを投げてみます。
> curl http://localhost:8080/ {"Title":"Hello","Desc":"BuildPack","Ver":"0.0.1"}
意図したレスポンスが返却されてきました。
Cloud Runへのデプロイ
以下のコマンドでcloudrunへデプロイする。 ポイントは--sourceオプション。
PORT番号については自動的にPORT環境変数が設定されており、デフォルトで8080が設定されている為、ここでは特に指定しない。
gcloud run deploy sample-app --region asia-northeast1 --platform managed --allow-unauthenticated --source .
しばらくするとデプロイ完了と共にURLが表示されると思うのでそこにリクエストを投げます。
Service URL: https://sample-app-aoytzwylua-an.a.run.app > curl https://sample-app-aoytzwylua-an.a.run.app main {"Title":"Hello","Desc":"BuildPack","Ver":"0.0.1"}
無事にデプロイできていました。
感想
裏側ではbuildpackによる、コンテナイメージが生成され、それがCloud Runにデプロイされているようでした。 便利。
entでgroup byで集約した結果のcountでorder byする方法
タイトルどおり。
select count(*) as cnt from items group by type order by cnt desc
みたいにgroup byして指定した項目毎に件数で並び替えたい、というケースがある場合、entではどうやったら記述できるのか調べた。
本当にこれが正解なのかはわからなかったけど、一応上のようなクエリをentで発行することはできた。
var results []struct { Count int Type string } err = r.ent().Item.Query().Where(apply).Order( func(s *sql.Selector) { s.OrderExpr(sql.ExprP("Count(*) Desc")) }, ).GroupBy(item.FieldType).Aggregate(ent.Count()).Scan(ctx, &results)
でも、もっといいやり方がありそうで、少しモヤル。
追記: 特に試してはいないけど、entでコード生成した時にすでに上記のようなものが生成されている模様。 だから、自前で上記のようなコードは記述する必要はなさそう。
参考
yarnでバージョンを切り替えられない現象への対応
yarnコマンドについては自前のPCでは特にv3系のしていて特に不便は感じてなくてそのままだった。 ただ、仕事関連のフロントエンドがv1系だったので、仕事の時だけ切り替える必要が出てきた。
最近のyarnは自前でバージョンを切り替え可能なようで、早速切り替えてみても切り替わらなかった。 下のような状況。
yarn set version 1.22.19 yarn -v 3.4.1
なぜだー、と思いながら、調べていた結果、$HOMEの.yarnrcと.yarnrc.ymlが原因っぽいことがわかった。
思い切って$HOMEの.yarnrcと.yarnrc.ymlを削除してみたら、やっと意図どおり動作した。 1時間ぐらいこれだけで時間が溶けてしまった・・・
yarn set version 1.22.19 yarn -v 1.22.19
viteで開発サーバを起動しても何も表示されなくなる現象への対応
仕事のフロントエンド開発ではviteを使用しています。
ただ最近viteコマンドで開発サーバを起動しても何も表示されなくなる(画面は真っ白)、という事象に遭遇しました。 chromeのコンソールを確認すると以下のような文字列だけが表示されていました。
Failed to load resource: the server responded with a status of 504 (Outdated Optimize Dep)
対応1
とりあえず、再インストールしたらどうにかなるかも、と思い、以下のような手順で再インストールし、viteを起動しました。
rm -fr node_modules yarn install yarn dev (viteコマンドを叩いているだけ)
これで画面が表示され、めでたしめでたし・・・とはなりませんでした。 一度、開発サーバを止めるとまた、この手順で起動しないと何も表示されない、という状態です。 これではツライ、ということでさらに調査。
対応2
結論としてはvite --forceでとりあえずは解決しました。
vite --force
根本的な原因についてはよくわからず、というのが正直なところ。
--forceオプションは強制的に再バンドルさせる指定のようです。 ただ、このオプションを指定するとviteの良さをなくすことになりそう・・・