まずは、公式ドキュメントのZero Allocationの項目を読む。
ここに記載されている内容(発生原因と解決策)は、公式ドキュメントで書いてある内容を、再度書いているようなもの。公式ドキュメントを読んだのであれば、実際に値が変わる様子を見たい人のみ、以下を読む。
fiber.Ctxで返される値が変化してしまう問題とは
fiber.Ctx
は、デフォルトでは、not immutableな値を返す。それをハンドラ外で保持するしており、複数のリクエストを受けた場合、値が書き変わってしまう可能性がある。
そのため、fiber.Ctx
から得られる、params(ctx.Params(key)
)などは、ハンドラ外で使用する場合、注意して使用する必要がある。
問題の挙動を見てみる
func main() {
addr := "localhost:3000"
go server(addr)
time.Sleep(3 * time.Second) // サーバ起動の待ち
client(addr)
time.Sleep(5 * time.Second) // wait関数の待ち
}
func client(addr string) {
eg := errgroup.Group{}
for i := 0; i < 10; i++ {
i := i
eg.Go(func() error {
u := fmt.Sprintf("http://%s/users/%d", addr, i)
_, err := http.Get(u)
return err
})
}
_ = eg.Wait()
}
func server(addr string) {
app := fiber.New()
app.Get("/users/:id", func(c *fiber.Ctx) error {
go wait(c.Params("id"))
return c.SendString("OK")
})
app.Listen(addr)
}
func wait(text string) {
time.Sleep(2 * time.Second)
fmt.Println(text)
}
結果
3
5
6
9
7
0
4
1
1
0
0が2つ、1が2つ出力されてしまっている
原因
公式ドキュメントで説明されている通り、fiberのCtxから返される値は、not immutableである。
そのため、ハンドラ外で値を使用していると、途中で変化してしまう可能性がある。
今回だと、go wait(c.Params("id"))
。
もう少し調べる
CopyString()
を利用して調べる。
CopyString()
を初めて見ると言う人は、この記事の冒頭で言っているのにも関わらず、公式ドキュメントを読んでいない人だと思うので、公式ドキュメントを読むことを勧めます。
// ハンドラ内のwait関数実行を以下に書き換える
go wait(utils.CopyString(c.Params("id")), c.Params("id"))
// wait関数を書き換える
func wait(a, text string) {
b := utils.CopyString(a)
time.Sleep(2 * time.Second)
fmt.Printf("%s, %s -> %s\n", a, b, text)
}
出力結果
// ハンドラ内でCopy、wait関数内でCopy -> wait関数の引数
2, 2 -> 2
5, 5 -> 5
8, 8 -> 5
4, 4 -> 5
3, 3 -> 3
9, 9 -> 9
7, 7 -> 7
1, 1 -> 1
0, 0 -> 1
6, 6 -> 6
これからわかるように、utils.CopyString()
でコピーした値は、1度しか出力されていないが、c.Params()
で得られた値は、重複している。また、wait関数内でも途中で変更されている。例えば、8, 8 -> 5
最後に
公式ドキュメントをよく読むこと。そして遊ぶこと。
もちろん、ハンドラ内で値が閉じている場合は、Ctxから得られて値を使用すればいい。今回だと、go wait(c.Params("id"))
となっているが、wait(c.Params("id"))
と、なっていれば、ハンドラ内で実行されているので、問題はない。