顔採点AIくんを個人開発して得たいくつかのgoの知見

Goの勉強がてらアプリを作ってみたので、いくつかのGoのライブラリについて書いてみようと思います。

## 作ったもの

顔採点AIくん
https://line.me/R/ti/p/@659rlati

![actionpreview.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3047864/cc88e86b-a90f-03c0-ea21-eabb41ea35b2.gif)

## `net/http`

Goでは、`net/http`という標準ライブラリだけでサーバーを起動させることができます。
が、結論として`/users/{id}`のように、変数を含んだパスを設定できないという致命的な壁が存在するので、[Gin](https://gin-gonic.com/ja/)とか使った方がいいです。

https://gin-gonic.com/ja/

以下のコードは`8080`ポートでサーバーを起動する例です。どんなパスに対しても`Hello`を返却します。

``` go
func main() {
	http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello")
	}))
}
```

パスによって処理は分けたいケースが多いはずなので、次のような書き方もできます。
この場合、`/hello`に対してリクエストを行うと、上と同じように`Hello`が返るのに対して、
`/`に対してリクエストを行うと`404 page not found`が返却されます。

``` go
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`を渡す場合、次のように簡略化できます。

``` go
func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello")
	})

	http.ListenAndServe(":8080", nil)
}
```

ネストした感じで書こうとすると`http.ServeMux`を使用してこうなります。
（`MUX`は[マルチプレクサ](https://www.google.com/search?q=multiplexer)の略とのこと。字面でピンと来なかったがググったらすぐイメージできた。）

どうにか`/good/` -> `/good/bye`のところを
`/good` -> `/bye`にしたかったが、無理なようです。

``` go
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)
	}))
}
```

そのかわり（？）、次のように書くことができます。

``` go
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`の定義を見てみると次のようになっているので、

``` go
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
```

`ServeHTTP`さえ実装してしまえばどんな構造体でも`Handle`や`ListenAndServe`に渡してしまうことができます。

``` go
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](https://gin-gonic.com/ja/)などのフレームワークを使用した方が良さそうです。

## `fmt`

`fmt`はめちゃくちゃお世話になります。

`fmt.Fprint`は`Writer`インターフェースに対して書き込みを行います。
`net/http`ではレスポンスボディの書き込みに使用していました。

``` go
func (Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Hello")
}
```

標準出力になにか表示したい場合は`fmt.Print*`

``` go
fmt.Println("Hello")
// => Hello

fmt.Printf("I am %s.", "Yamada Taro")
// => I am Yamada Taro.
```

文字列を生成したい場合は`fmt.Sprint*`

``` go
logger.Info(fmt.Sprintf("I am %s.", "Yamada Taro"))
```

`fmt.Errorf`は`error`を返却する関数です。

``` go
err := fmt.Errorf("user not found (id: %d)", id)
```

## `context`

APIサーバー設計ではmainの中に処理を全て書くことはおそらく少なくて、
処理の段階や役割ごとに関数やクラス（goにはクラスはないが）を適切に分割していくことが多い（はず）です。

そのようにしようと考えたときに、APIの入り口から出口、リクエストからレスポンスまで、データを持ち回すために`context`を使用することができます。

以下そのような例を書きます。
が、**簡略化のために良くないコードになっています**。
`context`のキーに文字列を使用することは非推奨です。IDEによってはワーニングが出るかと思います。

``` go
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`というパッケージ名で実験的に提供されている状態です。

以下のようにシンプルな呼び出しで標準出力へログを出力することができます。

``` go
slog.Info("message", "id", 1)
```

ファイルへ出力するには以下のようにします。

``` go
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形式で構造化したログを出力できたりもします。

``` go
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変換して保存する例です。

``` go
res, _ := http.Get("http://example.com/image.png")
img, _, _ := image.Decode(res.Body)
file, _ := os.Create("image.jpg")
jpeg.Encode(file, img, &jpeg.Options{})
```

画像の切り取りも標準パッケージで行うことができます。

``` go
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](https://developers.line.biz/ja/services/messaging-api/)を使用すると、
Webhookの受け口とWebAPIへのPOST処理を実装するだけで対話型のUIを作成することができます。

https://developers.line.biz/ja/services/messaging-api/

具体的には、ユーザーがLINEでメッセージを送信すると、指定のWebhook URLへ次のようなリクエストが飛んできます。

``` bash
{
  "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リクエストを送信すると、ユーザーへメッセージを送信することができます。

``` bash
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"
        }
    ]
}'
```

アーキテクチャとしては下の図のようになります。
アプリケーションサーバーではたいしたことしていないのが良くわかると思います。

``` mermaid
sequenceDiagram
  participant LINECli as LINEアプリ
  participant LINEServer as LINEプラットフォーム
  participant AppServer as アプリケーションサーバー
  participant MLServer as 画像解析SaaS
  LINECli->>+LINEServer: 画像送信
  LINEServer->>+AppServer: Webhook送信
  AppServer->>LINEServer: 画像リクエスト
  LINEServer->>AppServer: 画像データ
  AppServer->>+MLServer: 画像解析リクエスト
  MLServer->>-AppServer: 画像解析結果
  Note over AppServer: 処理日時、画像解析結果を保存
  Note over AppServer: LINEメッセージの作成
  AppServer->>-LINEServer: メッセージ送信リクエスト
  LINEServer->>-LINECli: 画像解析結果メッセージ
```

## おわり

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