公式ドキュメントすらあってるとは限らない (ポエム)

# TL;DR
公式ドキュメントも間違ってるときだってある。


# 公式ドキュメント通りにやってんのに動かねぇ！！
ある日私は、JWTを使った認証をGolangとechoで実装しようと考えました。
とりあえず何も分からなかったので、[echoの公式ドキュメント](https://echo.labstack.com/guide/)にあるCookbookから以下のコードをコピペしてGoに突っ込んでみました。
<details>
<summary>コピーしたコード</summary>
<div>

https://echo.labstack.com/cookbook/jwt/ より引用

```go
package main

import (
	"github.com/golang-jwt/jwt/v4"
	echojwt "github.com/labstack/echo-jwt/v4"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"net/http"
	"time"
)

// jwtCustomClaims are custom claims extending default ones.
// See https://github.com/golang-jwt/jwt for more examples
type jwtCustomClaims struct {
	Name  string `json:"name"`
	Admin bool   `json:"admin"`
	jwt.RegisteredClaims
}

func login(c echo.Context) error {
	username := c.FormValue("username")
	password := c.FormValue("password")

	// Throws unauthorized error
	if username != "jon" || password != "shhh!" {
		return echo.ErrUnauthorized
	}

	// Set custom claims
	claims := &jwtCustomClaims{
		"Jon Snow",
		true,
		jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)),
		},
	}

	// Create token with claims
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// Generate encoded token and send it as response.
	t, err := token.SignedString([]byte("secret"))
	if err != nil {
		return err
	}

	return c.JSON(http.StatusOK, echo.Map{
		"token": t,
	})
}

func accessible(c echo.Context) error {
	return c.String(http.StatusOK, "Accessible")
}

func restricted(c echo.Context) error {
	user := c.Get("user").(*jwt.Token)
	claims := user.Claims.(*jwtCustomClaims)
	name := claims.Name
	return c.String(http.StatusOK, "Welcome "+name+"!")
}

func main() {
	e := echo.New()

	// Middleware
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	// Login route
	e.POST("/login", login)

	// Unauthenticated route
	e.GET("/", accessible)

	// Restricted group
	r := e.Group("/restricted")

	// Configure middleware with the custom claims type
	config := echojwt.Config{
		NewClaimsFunc: func(c echo.Context) jwt.Claims {
			return new(jwtCustomClaims)
		},
		SigningKey: []byte("secret"),
	}
	r.Use(echojwt.WithConfig(config))
	r.GET("", restricted)

	e.Logger.Fatal(e.Start(":1323"))
}
```

</div>
</details>

コードをコピーして...
依存関係をgetして...
コンパイルして......
**動かねえ！！**

# エラーとの格闘
`/login`は動作するのに、`/restricted`を使おうとするとgo側が長文エラーを吐き始めます。

<details>
<summary>長文エラー(ユーザー名など一部改変してあります)</summary>

```
{"time":"2023-05-22T00:24:04.737973449+09:00","level":"-","prefix":"echo","file":"recover.go","line":"113","message":"[PANIC RECOVER] interface conversion: interface {} is *jwt.Token, not *jwt.Token (types from different packages) goroutine 19 [running]:\ngithub.com/labstack/echo/v4/middleware.RecoverWithConfig.func1.1.1()\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/middleware/recover.go:93 +0x14e\npanic({0x6ee9e0, 0xc00011bef0})\n\t/usr/lib/go-1.20/src/runtime/panic.go:884 +0x213\ntest/accessible({0x7c0558, 0xc0000abcc0})\n\t/projectpath/main.go:57 +0xd9\ngithub.com/labstack/echo-jwt/v4.Config.ToMiddleware.func2.1({0x7c0558, 0xc0000abcc0})\n\t/home/user/go/pkg/mod/github.com/labstack/echo-jwt/v4@v4.2.0/jwt.go:237 +0x41d\ngithub.com/labstack/echo/v4.(*Echo).add.func1({0x7c0558, 0xc0000abcc0})\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/echo.go:575 +0x51\ngithub.com/labstack/echo/v4/middleware.RecoverWithConfig.func1.1({0x7c0558, 0xc0000abcc0})\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/middleware/recover.go:119 +0xfe\ngithub.com/labstack/echo/v4/middleware.LoggerWithConfig.func2.1({0x7c0558, 0xc0000abcc0})\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/middleware/logger.go:126 +0xe2\ngithub.com/labstack/echo/v4.(*Echo).ServeHTTP(0xc0000c66c0, {0x7bcf70?, 0xc00014a000}, 0xc000100200)\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/echo.go:662 +0x3d1\nnet/http.serverHandler.ServeHTTP({0xc00011bc20?}, {0x7bcf70, 0xc00014a000}, 0xc000100200)\n\t/usr/lib/go-1.20/src/net/http/server.go:2936 +0x316\nnet/http.(*conn).serve(0xc0000fc2d0, {0x7bd198, 0xc00011bb30})\n\t/usr/lib/go-1.20/src/net/http/server.go:1995 +0x612\ncreated by net/http.(*Server).Serve\n\t/usr/lib/go-1.20/src/net/http/server.go:3089 +0x5ed\n\ngoroutine 1 [IO wait]:\ninternal/poll.runtime_pollWait(0x7f7d96c1dff8, 0x72)\n\t/usr/lib/go-1.20/src/runtime/netpoll.go:306 +0x89\ninternal/poll.(*pollDesc).wait(0xc0000fea00?, 0x4?, 0x0)\n\t/usr/lib/go-1.20/src/internal/poll/fd_poll_runtime.go:84 +0x32\ninternal/poll.(*pollDesc).waitRead(...)\n\t/usr/lib/go-1.20/src/internal/poll/fd_poll_runtime.go:89\ninternal/poll.(*FD).Accept(0xc0000fea00)\n\t/usr/lib/go-1.20/src/internal/poll/fd_unix.go:614 +0x2bd\nnet.(*netFD).accept(0xc0000fea00)\n\t/usr/lib/go-1.20/src/net/fd_unix.go:172 +0x35\nnet.(*TCPListener).accept(0xc0000a8810)\n\t/usr/lib/go-1.20/src/net/tcpsock_posix.go:148 +0x25\nnet.(*TCPListener).AcceptTCP(0xc0000a8810)\n\t/usr/lib/go-1.20/src/net/tcpsock.go:284 +0x3d\ngithub.com/labstack/echo/v4.tcpKeepAliveListener.Accept({0x4410a0?})\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/echo.go:982 +0x1d\nnet/http.(*Server).Serve(0xc000130000, {0x7bcd60, 0xc0000b4180})\n\t/usr/lib/go-1.20/src/net/http/server.go:3059 +0x385\ngithub.com/labstack/echo/v4.(*Echo).Start(0xc0000c66c0, {0xc00009eb9a, 0x5})\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/echo.go:679 +0xce\nmain.main()\n\t/projectpath/main.go:87 +0x5a6\n\ngoroutine 20 [runnable]:\nsyscall.Syscall(0x0?, 0x0?, 0x4ef052?, 0x7ffff800000?)\n\t/usr/lib/go-1.20/src/syscall/syscall_linux.go:69 +0x27\nsyscall.read(0xc0000fea80?, {0xc00011bc31?, 0x0?, 0x0?})\n\t/usr/lib/go-1.20/src/syscall/zsyscall_linux_amd64.go:711 +0x45\nsyscall.Read(...)\n\t/usr/lib/go-1.20/src/syscall/syscall_unix.go:178\ninternal/poll.ignoringEINTRIO(...)\n\t/usr/lib/go-1.20/src/internal/poll/fd_unix.go:794\ninternal/poll.(*FD).Read(0xc0000fea80?, {0xc00011bc31?, 0x1?, 0x1?})\n\t/usr/lib/go-1.20/src/internal/poll/fd_unix.go:163 +0x2ce\nnet.(*netFD).Read(0xc0000fea80, {0xc00011bc31?, 0x0?, 0x0?})\n\t/usr/lib/go-1.20/src/net/fd_posix.go:55 +0x29\nnet.(*conn).Read(0xc0000b41a0, {0xc00011bc31?, 0x0?, 0x0?})\n\t/usr/lib/go-1.20/src/net/net.go:183 +0x45\nnet/http.(*connReader).backgroundRead(0xc00011bc20)\n\t/usr/lib/go-1.20/src/net/http/server.go:674 +0x3f\ncreated by net/http.(*connReader).startBackgroundRead\n\t/usr/lib/go-1.20/src/net/http/server.go:670 +0xca\n\n"}
{"time":"2023-05-22T00:24:04.738101663+09:00","id":"","remote_ip":"127.0.0.1","host":"localhost:1323","method":"GET","uri":"/restricted","user_agent":"curl/7.81.0","status":500,"error":"","latency":367498,"latency_human":"367.498µs","bytes_in":0,"bytes_out":36}
```
</details>

**う～ん、わからんｗ！**

長文エラーが一瞬にして吐かれたので、頭の使用率は100%です。理解できません。

とりあえず最初の方に`[PANIC RECOVER] interface conversion: interface {} is *jwt.Token, not *jwt.Token`
と表示されてるので、型アサーション系のエラーということだけはわかりました。
どう見ても`*jwt.Token`と`*jwt.Token`って同じなのに何が違うんや??と思い色々調べました。
(今見たら`not *jwt.Token~`の後に`(types from different packages)`って書いてありますね...これに気づいてたらもっと早く解決できてたかもしれません。)

エラーについて調べて`go mod tidy`をしてみたり、コードの順番を入れ替えてみたり、OSを変えてみたり...
あれこれ試しているうちに一日が過ぎました。

そして、この[issue](https://github.com/dgrijalva/jwt-go/issues/401)にたどり着きます。
このissueについてるコメントに、

> `echo`で使ってるjwtと、`import`で使うjwtのバージョンを合わせてくれよな！(超絶意訳)

と書いてあり、まさかとは思いながら[echo-jwt](https://github.com/labstack/echo-jwt)のREADMEを読んでみました。
するとどうやら、最近`echo-jwt`が使う`jwt`のバージョンが`v4`から`v5`に変わっているようでした。

そこでコード内の`import`で使っているjwtのバージョンを`v5`にすると

**動いた！！！**

どうやらバージョン違いの問題で一日を潰したようです。
**~~同じ作成者なら依存関係に破壊的なアップデートをしたときにドキュメントも更新してくれ！！~~**

ということで問題になっていたドキュメントを修正して、GithubでPull Requestを出しました。

# 教訓
- 公式ドキュメントも万能じゃない。エラーが起きたらバージョン違いなども視野に入れて解決しよう。
- エラー文はちゃんと読もう。

ここまで読んでいただいてありがとうございました！