Goの勉強がてらアプリを作ってみたので、いくつかのGoのライブラリについて書いてみようと思います。
作ったもの
顔採点AIくん
https://line.me/R/ti/p/@659rlati
net/http
Goでは、net/http
という標準ライブラリだけでサーバーを起動させることができます。
が、結論として/users/{id}
のように、変数を含んだパスを設定できないという致命的な壁が存在するので、Ginとか使った方がいいです。
以下のコードは8080
ポートでサーバーを起動する例です。どんなパスに対してもHello
を返却します。
func main() {
http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello")
}))
}
パスによって処理は分けたいケースが多いはずなので、次のような書き方もできます。
この場合、/hello
に対してリクエストを行うと、上と同じようにHello
が返るのに対して、
/
に対してリクエストを行うと404 page not found
が返却されます。
func main() {
http.Handle("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello")
}))
http.ListenAndServe(":8080", nil)
}
また、http.Handle
にhttp.HandlerFunc
を渡す場合、次のように簡略化できます。
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello")
})
http.ListenAndServe(":8080", nil)
}
ネストした感じで書こうとするとhttp.ServeMux
を使用してこうなります。
(MUX
はマルチプレクサの略とのこと。字面でピンと来なかったがググったらすぐイメージできた。)
どうにか/good/
-> /good/bye
のところを
/good
-> /bye
にしたかったが、無理なようです。
func main() {
mux := http.NewServeMux()
mux.Handle("/good/", func() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/good/bye", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Goodbye")
})
return mux
}())
http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mux.ServeHTTP(w, r)
}))
}
そのかわり(?)、次のように書くことができます。
func main() {
mux := http.NewServeMux()
mux.Handle(goodRouter())
http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mux.ServeHTTP(w, r)
}))
}
func goodRouter() (string, http.Handler) {
mux := http.NewServeMux()
mux.HandleFunc("/good/bye", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Goodbye")
})
mux.HandleFunc("/good/evening", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Goodevening")
})
return "/good/", mux
}
また、http.Handler
の定義を見てみると次のようになっているので、
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ServeHTTP
さえ実装してしまえばどんな構造体でもHandle
やListenAndServe
に渡してしまうことができます。
func main() {
http.ListenAndServe(":8080", NewRouter())
}
type Router struct{}
func NewRouter() Router {
return Router{}
}
func (Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello")
}
このように、(頑張れば)標準のnet/http
のAPIだけで実装の幅は結構広そうです。
が、冒頭にも書いた通り調べた範囲では/users/{id}
のように、パスに含んだ変数を取得するようなAPIがありませんでしたので、RESTfulなURL設計がしたい場合はGinなどのフレームワークを使用した方が良さそうです。
fmt
fmt
はめちゃくちゃお世話になります。
fmt.Fprint
はWriter
インターフェースに対して書き込みを行います。
net/http
ではレスポンスボディの書き込みに使用していました。
func (Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello")
}
標準出力になにか表示したい場合はfmt.Print*
fmt.Println("Hello")
// => Hello
fmt.Printf("I am %s.", "Yamada Taro")
// => I am Yamada Taro.
文字列を生成したい場合はfmt.Sprint*
logger.Info(fmt.Sprintf("I am %s.", "Yamada Taro"))
fmt.Errorf
はerror
を返却する関数です。
err := fmt.Errorf("user not found (id: %d)", id)
context
APIサーバー設計ではmainの中に処理を全て書くことはおそらく少なくて、
処理の段階や役割ごとに関数やクラス(goにはクラスはないが)を適切に分割していくことが多い(はず)です。
そのようにしようと考えたときに、APIの入り口から出口、リクエストからレスポンスまで、データを持ち回すためにcontext
を使用することができます。
以下そのような例を書きます。
が、簡略化のために良くないコードになっています。
context
のキーに文字列を使用することは非推奨です。IDEによってはワーニングが出るかと思います。
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "uuid", uuid.NewString())
db, _ := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test_db")
ctx = context.WithValue(ctx, "db", db)
http.ListenAndServe(":8080", NewRouter(ctx))
}
type Router struct {
ctx context.Context
}
func NewRouter(ctx context.Context) Router {
return Router{
ctx: ctx,
}
}
func (rr Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, rr.ctx.Value("uuid"))
}
context
には、go routineで実行される処理へキャンセルの意思を伝播するなど出来るようですが、go routineについてはあまり検証できていないため、ここでは書かないことにします。
golang.org/x/exp/slog
goでログを出力するためには、標準のlog
パッケージがありますが、近いうちにlog/slog
というパッケージが標準に追加されるようですので、こちらを使用するようにしたほうがいい気がしています。
現在はlog/slog
ではなく、golang.org/x/exp/slog
というパッケージ名で実験的に提供されている状態です。
以下のようにシンプルな呼び出しで標準出力へログを出力することができます。
slog.Info("message", "id", 1)
ファイルへ出力するには以下のようにします。
logFile, err := os.Create("/var/log/debug.log")
if err != nil {
panic(err)
}
logger := slog.New(slog.HandlerOptions{}.NewTextHandler(logFile))
logger.Info("message", "id", 1)
slog
ではJSON形式で構造化したログを出力できたりもします。
logger := slog.New(slog.HandlerOptions{}.NewJSONHandler(os.Stderr))
logger.Info("json sample", slog.Any("struct", struct {
Name string
Children []struct{ Name string }
}{
Name: "Kimura Taro",
Children: []struct{ Name string }{
{Name: "Kimura Hanako"},
{Name: "Kimura Ichiro"},
},
}))
// => {"time":"2023-03-27T12:29:30.708735237Z","level":"INFO","msg":"json sample","struct":{"Name":"Kimura Taro","Children":[{"Name":"Kimura Hanako"},{"Name":"Kimura Ichiro"}]}}
image
画像処理をPHPでやろうとすると、gd
やimagemagick
を使用する必要がありますが、
goでは標準でimage
というパッケージが入っています。
以下httpで取得したpng画像をjpg変換して保存する例です。
res, _ := http.Get("http://example.com/image.png")
img, _, _ := image.Decode(res.Body)
file, _ := os.Create("image.jpg")
jpeg.Encode(file, img, &jpeg.Options{})
画像の切り取りも標準パッケージで行うことができます。
original, _ := os.Open("image.jpg")
originalImg, _, _ := image.Decode(original)
type SubImager interface {
SubImage(image.Rectangle) image.Image
}
croppedImg := originalImg.(SubImager).SubImage(image.Rect(0, 0, 50, 50))
cropped, _ := os.Create("cropped.jpg")
jpeg.Encode(cropped, croppedImg, &jpeg.Options{})
調べた感じだと画像合成や文字入れなども標準パッケージでできそうです。
LINE Messaging APIについて
goとは関係ないですが。
今回はgoにフォーカスして試してみたかったので、UI関連はすべてLINEにおまかせになっています。
LINE Messaging APIを使用すると、
Webhookの受け口とWebAPIへのPOST処理を実装するだけで対話型のUIを作成することができます。
具体的には、ユーザーがLINEでメッセージを送信すると、指定のWebhook URLへ次のようなリクエストが飛んできます。
{
"destination": "xxxxxxxxxx",
"events": [
{
"type": "message",
"message": {
"type": "text",
"id": "14353798921116",
"text": "Hello, world"
},
"timestamp": 1625665242211,
"source": {
"type": "user",
"userId": "U80696558e1aa831..."
},
"replyToken": "757913772c4646b784d4b7ce46d12671",
"mode": "active",
"webhookEventId": "01FZ74A0TDDPYRVKNK77XKC3ZR",
"deliveryContext": {
"isRedelivery": false
}
}
]
}
また、LINE APIの指定エンドポイントへPOSTリクエストを送信すると、ユーザーへメッセージを送信することができます。
curl -v -X POST https://api.line.me/v2/bot/message/push \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {channel access token}' \
-H 'X-Line-Retry-Key: {UUID}' \
-d '{
"to": "U4af4980629...",
"messages":[
{
"type":"text",
"text":"Hello, world"
}
]
}'
アーキテクチャとしては下の図のようになります。
アプリケーションサーバーではたいしたことしていないのが良くわかると思います。
おわり
goはtry-catchがなかったりエラー処理が冗長だったりと辛い部分もありますが、書いていて楽しかったです。
開発体験が良かったのは、IDE (Visual Studio Code) とLanguage Serverの助力が大きかったと思います。
コンパイルエラーになる部分を事前に教えてくれたり適切にフォーマットしてくれたり、補完機能やジャンプ機能には大変お世話になりました。
また、VSCode Serverによるトンネリングにより、VPS内のDockerコンテナにMacbookのVSCodeから接続して開発をすることができました。
(Webhookにグローバルなドメインを使用する都合上、開発環境がlocalhostになっていると辛い)
これらがなければ早々に心折れていた気がします。
Visual Studio Codeも、Language Serverも、今ではGitHubも、全てMicrosoftのプロダクトですね。
一昔前はナードなイケてない企業のイメージだったのですが、いつからこうなったのでしょうか。
最後に今回作成した顔採点AIくんのリンクをもう一度貼って終わります。
内容の誤り、補足等ございましたらコメントいただけると嬉しいです。
ここまで読んでいただきありがとうございました。
顔採点AIくん
https://line.me/R/ti/p/@659rlati