Go基礎 #7 パッケージとモジュール (go mod)

基礎シリーズの最終回。これまでのコードは1つのファイルで生きていましたが、実プロジェクトは複数のファイルと外部ライブラリが一緒に動きます。今回は — Goのコード組織化システム

2つの単位 — パッケージとモジュール #

パッケージ (package)モジュール (module)
単位1フォルダ複数フォルダの束ね
定義package 名前 宣言go.mod ファイル
例えクラス/名前空間プロジェクト/ライブラリ

1フォルダ = 1パッケージ複数フォルダ + go.mod = 1モジュール。モジュールがより大きい単位です。

パッケージ — 1フォルダが1単位 #

同じフォルダのファイルはすべて同じパッケージに属さなければなりません。

フォルダ構造
calculator/
├── add.go         (package calculator)
├── multiply.go    (package calculator)
└── helper.go      (package calculator)

すべて package calculator で始まらなければなりません。違うパッケージ名が混ざるとコンパイルエラー。

mainパッケージ — 実行可能なプログラム #

main パッケージ
package main

import "fmt"

func main() {
	fmt.Println("hi")
}

package main + func main() があるフォルダは実行可能なプログラム。それ以外のパッケージはライブラリとして他のコードがimportして使うコードです。

可視性 — 大文字始まり #

Goの可視性は非常にシンプルです。

名前が大文字で始まれば外部に公開(exported)、小文字ならパッケージ内でのみ可視(unexported)。

可視性
package math

// 外部から使用可能
func Add(a, b int) int {
	return a + b
}

// 同じパッケージ内のみ使用可能
func square(n int) int {
	return n * n
}

// 外部公開定数
const Pi = 3.14159

// 内部定数
const initialBuffer = 1024

他の言語のような public/private キーワードは別途ありません。名前の最初の文字を見るだけで可視性が分かります。

import — 他のパッケージを取り込む #

標準ライブラリ #

標準 import
import "fmt"
import "strings"
import "time"

// またはグループ形式で
import (
	"fmt"
	"strings"
	"time"
)

複数のimportはグループ形式が推奨されます。アルファベット順のソートは goimports が自動でやってくれます。

外部パッケージ #

外部パッケージ
import (
	"github.com/google/uuid"
	"github.com/spf13/cobra"
)

GitHub URLのような形がそのままimport pathになります。Goのモジュールシステムがurlベースである理由です。

エイリアスとドットimport #

エイリアス
import (
	f "fmt"               // エイリアス
	. "strings"           // ドット — パッケージ名なしで直接使用 (非推奨)
	_ "github.com/.../init"   // 空識別子 — 副作用のみ (init 関数を実行)
)

f.Println("hi")

エイリアスは同じ名前の衝突時に有用。ドットimportはコードがどこから来た関数かわかりにくくなるのでほとんど使いません。空識別子(_)はパッケージのinit関数だけ実行したいとき(ドライバ登録など)。

モジュール — go.mod #

複数のパッケージをまとめた単位。go.mod ファイルが1つのモジュールのエントリーポイントです。

新規モジュール開始
mkdir myapp
cd myapp
go mod init github.com/curtis/myapp

go.mod が作られます。

go.mod
module github.com/curtis/myapp

go 1.22
  • module — このモジュールのimport path。他のコードが import "github.com/curtis/myapp/..." のように取り込みます。
  • go — 使用するGoの最小バージョン。

モジュール名 — なぜGitHubパスなのか? #

GoはモジュールをURLで識別します。通常GitHub/GitLabのリポジトリパスがそのままモジュール名になります。

よくあるパターン
module github.com/curtis/myapp
module gitlab.com/team/service
module example.com/internal/utils    // 私的ドメイン

GitHubに上げる予定がなくてもこの形式に従います。Goが依存関係をダウンロードするとき、そのURLに行って取得します(公開リポジトリの場合)。

複数のパッケージを持つモジュール #

複数パッケージ
myapp/
├── go.mod   (module github.com/curtis/myapp)
├── main.go  (package main)
├── auth/
│   ├── login.go      (package auth)
│   └── register.go   (package auth)
└── db/
    └── connection.go (package db)

main.go で他のパッケージを使う:

main.go
package main

import (
	"fmt"
	"github.com/curtis/myapp/auth"
	"github.com/curtis/myapp/db"
)

func main() {
	conn := db.Connect()
	user := auth.Login("カーティス", "secret")
	fmt.Println(conn, user)
}

同じモジュール内のパッケージは — モジュール名 + フォルダパスでimport。

外部パッケージ追加 — go get #

パッケージ追加
go get github.com/google/uuid

このコマンドが:

  1. パッケージをダウンロード
  2. go.mod に依存関係を追加
  3. go.sum にチェックサムを記録
go.mod 更新
module github.com/curtis/myapp

go 1.22

require github.com/google/uuid v1.6.0

go.sum は依存関係の正確なバージョンを記録したファイル — 他の人が同じコードをビルドしても正確に同じバージョンが使われるよう保証します。

使用 #

外部パッケージ使用
package main

import (
	"fmt"
	"github.com/google/uuid"
)

func main() {
	id := uuid.New()
	fmt.Println(id)
}

go mod tidy — 整理 #

依存関係の整理
go mod tidy

コードを解析して:

  • 使用中なのにgo.modにない依存関係 → 追加
  • go.modにあるのに使ってない依存関係 → 削除

作業ごとに一度実行する習慣がよいです。

依存関係のバージョン — semver #

バージョン表記
require github.com/google/uuid v1.6.0

semver(セマンティックバージョニング) — vメジャー.マイナー.パッチ。Goモジュールシステムは次の規則に従います。

  • メジャー0 — 安定保証なし (実験)
  • メジャー1+ — 互換保証 (マイナー/パッチ更新は互換)
  • メジャー2+ — import pathに /v2/v3 のようなsuffix必須
v2 以上の import
import "github.com/some/lib/v2"

ライブラリがv2に上がるとimport path自体が変わります。下位互換を強制する設計です。

バージョン更新 #

更新
# パッケージを最新に
go get github.com/google/uuid@latest

# 特定バージョン
go get github.com/google/uuid@v1.5.0

# すべての依存関係を最新に
go get -u ./...

# マイナー/パッチのみ最新に
go get -u=patch ./...

同じモジュール内 — internalパッケージ #

internal フォルダ
myapp/
├── go.mod
├── main.go
├── api/
│   └── handler.go        (package api)
└── internal/
    └── auth/
        └── login.go      (package auth)

internal/ フォルダ内のパッケージは — そのモジュールまたはそのモジュールの子孫だけがimport可能。外部モジュールは取り込めません。

ライブラリを作るとき — 公開APIと実装詳細を分離する標準パターンです。

パッケージinit関数 #

各パッケージは — 最初にimportされるとき自動実行される init() 関数を持てます。

init
package config

import "log"

var Settings map[string]string

func init() {
	Settings = loadFromFile()
	log.Println("config 読み込み完了")
}

副作用(設定の読み込み、ドライバ登録)に使います。1パッケージに複数の init も可能です。

濫用は避けましょう — 明示的な初期化関数(Init())の方が普通明確です。

vendorフォルダ — オプション #

go mod vendor コマンドで依存関係をプロジェクト内にコピーできます。

vendor
go mod vendor

vendor/ フォルダにすべての依存関係コードが入ります。ビルド時にそこから取得します。

長所: 外部ダウンロードなしでビルド可能 (オフライン、私設ネットワークで)。短所: リポジトリが大きくなります。

最近は go.mod + ローカルキャッシュ($GOPATH/pkg/mod)で十分なので、vendorはあまり使わない傾向です。

replace — ローカルパスで置換 #

ライブラリ開発中 — 実際の依存関係をローカルパスに一時的に置換。

go.mod
require github.com/curtis/lib v1.0.0

replace github.com/curtis/lib => ../lib

../lib フォルダのコードが使われます。ライブラリとそれを使うアプリを同時に開発するときに便利です。コミット前に普通は削除します。

よく使うコマンドまとめ #

モジュールコマンド
go mod init <名前>      # 新規モジュール
go mod tidy             # 依存関係の整理
go mod download         # 明示された依存関係のダウンロードのみ
go get <パッケージ>      # 依存関係の追加
go get -u <パッケージ>   # 更新
go mod why <パッケージ>   # なぜこの依存関係が必要か
go mod graph             # 依存関係グラフの出力
go mod vendor            # vendor フォルダを作成

軽いガイド — パッケージ設計 #

大きな設計ガイドを全部整理するのは難しいですが、よく役立ついくつか。

  1. パッケージ名は短く小文字authdbconfig
  2. パッケージが複数のことをするなら 細かく分割
  3. 循環依存(A → B、B → A)はコンパイルエラー — 設計が絡まったサイン
  4. 外部公開は最小化 — できるだけ小文字始まり
  5. utilcommon のような雑多パッケージは避ける — 意図が見えない

まとめ #

基礎シリーズ7編を通して扱った内容:

  1. はじめてのプログラム — セットアップとHello World (#1)
  2. 変数、型、定数:=、iota、untyped const (#2)
  3. 制御フロー — forが唯一、switch fallthroughなし (#3)
  4. 関数、多値返却、errorif err != nil、defer (#4)
  5. コレクション — sliceの動作原理、mapのcomma-ok (#5)
  6. 構造体とメソッド — 値/ポインタレシーバ、埋め込み (#6)
  7. パッケージとモジュール — 可視性、go.mod、internal (今回の記事)

ここまで掴めば小さなプログラムやCLIツールは自信を持って作れます。次の中級シリーズでは — インターフェース、並行性(ゴルーチン/チャネル)、コンテキスト、標準ライブラリといったGoの真の強みを扱います。

X