goでモジュールをダウンロードする際の挙動について調べてみる

goでモジュールをダウンロードする仕組みってどうなっているか知っていますか？

`go mod init`でモジュールを初期化して、`go get`で依存関係に追加してみたいなことをやっていると思います。

ですが`node_modules`のように、どこにmoduleが入ってるかってわからないですよね。

今回はgoでモジュールをダウンロードする際の挙動について調べてみたいと思います。

# goもモジュールをダウンロードする際の流れ

goでモジュールをダウンロードするには、`go get`や`go mod tidy`を使用してモジュールをダウンロードしていたと思います。

まず、ダウンロードする際には`proxy.golang.org`と`sum.golang.org`、`index.golang.org`が使用されています。

|ドメイン|役割
|---|---
|proxy.golang.org|モジュールのキャッシュプロキシ
|sum.golang.org|モジュールの整合性を図るチェックサムDB
|index.golang.org|モジュールのver一覧を表示

## proxy.golang.org
これはgoが持っているキャッシュプロキシで`Module Mirror`(モジュールのミラーサーバー)と呼ばれています。

:::note

### ミラーサーバーとは
別の言い方でリバースプロキシと呼ばれているものです。

プロキシとは、クライアントとサーバーの仲介役を担うサーバーのことを指し、サーバーの負荷分散やファイアーウォールの役割をします。
:::

`Module Mirror`は、github等にあるgoのモジュールをダウンロードしてユーザーに渡した際に、そのモジュールをキャッシュするといった仕組みになっています。

`Module Mirror`を使うことで以下のメリットがあります。

- モジュールダウンロードの高速化
よく使われるモジュールがキャッシュされることで、githubから検索してダウンロードするといったレイテンシが小さくなります。

- 可用性
github等が障害で使えなくてもダウンロードすることができるようになります。


- 不変性の保証
一度キャッシュされたモジュールはそこから更新されることがありません。
ModuleMirrorは特定のリポジトリとタグを紐づけてキャッシュしているので、更新するにはタグを変更してリポジトリにpushしておくことが必要になります。
またリポジトリが消えてもModuleMirrorからは消えず、キャッシュされているのでダウンロードすることができます。

:::note alert
### Module Mirrorを悪用した事例
この永続的にキャッシュする機構を逆手に取った攻撃があったそうです。
正規のモジュールに名前が似たバックドアが仕込まれているモジュールを公開し、そのモジュールがModule Mirrorにキャッシュされていました。
これによって悪意のあるモジュールが消されても残り続けるという問題が起きたそうです。

防ぐためにもしっかりとパスを確認してダウンロードする必要があると思います。
>https://www.infoq.com/jp/news/2025/05/google-go-module-mirror-backdoor/
:::

## sum.golang.org
モジュール用のChecksumDBです。

これはModuleMirrorを通してダウンロードしたモジュールのチェックサムとChecksumDBに保存されているチェックサムを比較することで、信頼性のあるコードかを検証することができます。


## index.golang.org
モジュールの最新verを見つけるためにある仕組みで、https://index.golang.org/index にアクセスすることでモジュールのverが管理されていることがわかります。


# 実際の挙動を見てみる
今回は`Module Mirror`を経由して、作成したモジュールをダウンロードしてみようと思います。

## モジュール作成
まずモジュールを初期化します。
```zsh
go mod init "github.com/maooz4426/hello
```
下のように名前を入れたら、その人に挨拶をする文面を作成するモジュールを作りました。
```go
package hello

import (
	"errors"
	"fmt"
	"log"
)

func Hello(name string) (string, error) {
	log.Println(name)
	if name != "" {
		return "", errors.New("empty name")
	}

	message := fmt.Sprintf("Hello %s", name)
	return message, nil
}
```
>ちなみにこのコードは正しく動きません(挨拶されずにエラーを吐きます)

できたら下のようにタグをつけてpushします。
```zsh
git tag v1.0.0
```
```zsh
git push origin v1.0.0
```

## モジュールをインポートする
先ほどのモジュールを動かすコードを書きます。
モジュールを初期化して外部モジュールをインポートできるようにします。
```zsh
go mod init "github.com/maooz4426/goproxy-test"
```
go.modの依存関係に追加します。
```zsh
go get github.com/maooz4426/hello@v1.0.0
```
>めっちゃダウンロードするのに時間がかかったので、Module Mirrorにキャッシュされていないのだと思いました。

ここから始めて取得したモジュールは`$GOPATH/pkd/mod/github.com/maooz4426/hello@v1.0.0`にダウンロードされる。

```go
package main

import (
	"fmt"
	"log"

	"github.com/maooz4426/hello"
)

func main() {
	mes, err := hello.Hello("maoz")
	log.Println(err)
	fmt.Println(mes)
}
```

実行すると失敗します(ただしモジュールは実行されるのでインポートはできてる)
```
❯ go run main.go
2025/06/05 20:24:32 empty name
```

# tagの中身を入れ替えてみる
テストコード書いて以下のように動くように調節した。
```hello_test.go
package hello

import (
	"testing"

	"github.com/stretchr/testify/require"
)

func Test_Hello(t *testing.T) {
	msg, err := Hello("test")
	require.NoError(t, err)
	require.Equal(t, msg, "Hello test")
}
```
```hello.go
package hello

import (
	"errors"
	"fmt"
	"log"
)

func Hello(name string) (string, error) {
	log.Println(name)
	if name == "" {
		return "", errors.New("empty name")
	}

	message := fmt.Sprintf("Hello %s", name)
	return message, nil
}
```
v1.0.0をこのコードに紐づけるため、上書きします
```zsh
git tag -f v1.0.0
git push -f origin v1.0.0
```


これでgo getしてみますが、挙動は変わりませんでした。

```
go get github.com/maooz4426/hello@v1.0.0
```
```zsh
❯ go run main.go
2025/06/06 14:16:10 empty name
```

先述したモジュールは`$GOPATH/pkd/mod/github.com/maooz4426/hello@v1.0.0`に入っています。
これを削除してから再モジュールダウンロードを行ってみました。

そして実行しても挙動が変わりませんでした。
```
❯ go run main.go
2025/06/06 14:16:43 empty name
```
>moduleを削除して`go mod tidy`等でモジュールをダウンロードするとわかるのですが、大分ダウンロードが早いです。

このようにModule Mirrorは一度キャッシュすると永続的にキャッシュをします。

ここからmoduleをアップデートしてみます。
```zsh
❯ go get github.com/maooz4426/hello@v1.0.1
go: downloading github.com/maooz4426/hello v1.0.1
go: upgraded github.com/maooz4426/hello v1.0.0 => v1.0.1
```
それで実行してみるとしっかり実行されるようになりました。
```zsh
❯ go run main.go
2025/06/08 18:01:41 maoz
2025/06/08 18:01:41 <nil>
Hello maoz
```

# まとめ

GoのModule Mirrorシステムは、モジュールの高速化、可用性、不変性を提供する優れた仕組みなのがわかりました。しかし、一度キャッシュされたモジュールは永続的に保持されるため、コードを修正する際は新しいバージョンタグを作成する必要があります。また、悪意のあるモジュールに対する注意も必要なので、正しいモジュールを指定できているか調べる必要がありそうです。