golang-idioms について
Go 開発者向けのコーディングリファレンススキルです。エラー、インターフェース、並行処理、テスト、モジュール、ロギングの慣用法に加え、コンテキスト伝播と型設計の指針も示します。
主な機能
- 01コンテキスト付きエラー処理
- 02小さく組み合わせ可能なインターフェース
- 03ゴルーチンとチャネルのパターン
- 04コンテキスト伝播
- 05テーブル駆動テストとヘルパー
- 06モジュール・型・ロギングの指針
活用シーン
- 01Go のエラー処理の確認
- 02キャンセル可能な並行処理の設計
- 03テーブル駆動テストの構成
- 04モジュールと型設計の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ rohitg00/ awesome-claude-code-toolkit --skill golang-idiomsインストール後、Claude Code で/golang-idiomsを入力して読み込まれたか確認します。
詳細
Go のイディオム
エラー処理
// Return errors, never panic in library code
func LoadConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("reading config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return Config{}, fmt.Errorf("parsing config: %w", err)
}
return cfg, nil
}
ルール:
fmt.Errorf("context: %w", err)を使って、常にエラーをコンテキスト付きでラップします- 呼び出し元が
errors.Isとerrors.Asを使えるように、%wを使用します - 適切なレベルでエラーを処理します。同じエラーをログに記録してから返してはいけません
- 想定される条件に対してセンチネルエラーを定義します
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
func GetUser(id string) (User, error) {
user, ok := store[id]
if !ok {
return User{}, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
return user, nil
}
// Caller
user, err := GetUser(id)
if errors.Is(err, ErrNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
インターフェース設計
// Keep interfaces small (1-3 methods)
type Reader interface {
Read(p []byte) (n int, err error)
}
type UserStore interface {
GetUser(ctx context.Context, id string) (User, error)
CreateUser(ctx context.Context, u User) error
}
// Accept interfaces, return structs
func NewService(store UserStore, logger *slog.Logger) *Service {
return &Service{store: store, logger: logger}
}
ルール:
- インターフェースは実装する側ではなく、使用する側(コンシューマー側)で定義します
- 大きなインターフェースよりも、小さく組み合わせ可能なインターフェースを優先します
- 標準ライブラリの
io.Reader、io.Writer、fmt.Stringerを使用します - メソッドが 1 つのインターフェースには、メソッド名に
erを付けた名前を付けます
ゴルーチンとチャネルのパターン
ワーカープール
func process(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
results := make(chan Result, workers)
var wg sync.WaitGroup
for range workers {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
select {
case <-ctx.Done():
return
case results <- job.Execute():
}
}
}()
}
go func() {
wg.Wait()
close(results)
}()
return results
}
ファンアウト/ファンイン
func fanOut[T, R any](ctx context.Context, items []T, fn func(T) R, concurrency int) []R {
sem := make(chan struct{}, concurrency)
results := make([]R, len(items))
var wg sync.WaitGroup
for i, item := range items {
wg.Add(1)
sem <- struct{}{}
go func() {
defer func() { <-sem; wg.Done() }()
results[i] = fn(item)
}()
}
wg.Wait()
return results
}
ルール:
- 常に
context.Contextを最初のパラメーターとして渡します - コンテキストのキャンセルまたはチャネルのクローズによって、ゴルーチンを常に停止できるようにします
- ゴルーチンの完了を待つには
sync.WaitGroupを使用します - プロデューサーとコンシューマーの処理速度が異なる場合は、バッファー付きチャネルを使用します
- 停止方法がわからないゴルーチンを起動してはいけません
コンテキストの伝播
func (s *Service) HandleRequest(ctx context.Context, req Request) (Response, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
user, err := s.store.GetUser(ctx, req.UserID)
if err != nil {
return Response{}, fmt.Errorf("getting user: %w", err)
}
ctx = context.WithValue(ctx, userKey, user)
return s.processRequest(ctx, req)
}
ルール:
- I/O を行うすべての関数で、コンテキストを最初のパラメーターとして渡します
- すべての外部呼び出しで
context.WithTimeoutまたはcontext.WithDeadlineを使用します - キャンセル可能なコンテキストを作成した後は、常に
defer cancel()を実行します context.WithValueの使用は必要最小限にします(トレース ID や認証情報など、リクエストスコープの値に限ります)- コンテキストを構造体に保存してはいけません
テーブル駆動テスト
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
want bool
}{
{"valid email", "[email protected]", true},
{"missing @", "userexample.com", false},
{"empty string", "", false},
{"multiple @", "user@@example.com", false},
{"valid with subdomain", "[email protected]", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateEmail(tt.email)
if got != tt.want {
t.Errorf("ValidateEmail(%q) = %v, want %v", tt.email, got, tt.want)
}
})
}
}
テストヘルパー
func newTestServer(t *testing.T) *httptest.Server {
t.Helper()
handler := setupRoutes()
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)
return srv
}
func assertEqual[T comparable](t *testing.T, got, want T) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
すべてのテストユーティリティ関数で t.Helper() を使用します。テストリソースのクリーンアップには defer ではなく t.Cleanup() を使用します。テスト用のフィクスチャには testdata/ ディレクトリを使用します。
モジュール管理
go.mod structure:
module github.com/org/project
go 1.23
require (
github.com/lib/pq v1.10.9
golang.org/x/sync v0.7.0
)
コマンド:
go mod tidy # remove unused, add missing
go mod verify # verify checksums
go list -m -u all # check for updates
go get -u ./... # update all dependencies
go mod vendor # vendor dependencies (optional)
コミットする前に、必ず go mod tidy を実行します。メジャーバージョンを固定します。更新前に変更履歴を確認します。
ゼロ値を活用した設計
ゼロ値が有用になるように型を設計します。
// sync.Mutex zero value is an unlocked mutex (ready to use)
var mu sync.Mutex
// bytes.Buffer zero value is an empty buffer (ready to use)
var buf bytes.Buffer
buf.WriteString("hello")
// Custom types: make zero value meaningful
type Server struct {
Addr string // defaults to ""
Handler http.Handler // defaults to nil
Timeout time.Duration // defaults to 0 (no timeout)
}
func (s *Server) ListenAndServe() error {
addr := s.Addr
if addr == "" {
addr = ":8080" // useful default
}
handler := s.Handler
if handler == nil {
handler = http.DefaultServeMux
}
// ...
}
ルール:
- コンストラクターよりも、意味のあるゼロ値を持つ構造体を優先します
- メソッドでレシーバーを変更する場合は、ポインターレシーバーを使用します
- メソッドで読み取りのみを行う場合は、値レシーバーを使用します
- ユーザーが直接設定すべきでないフィールドをエクスポートしてはいけません。代わりにコンストラクター関数を使用します
構造化ログ
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
logger.Info("request handled",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.Int("status", status),
slog.Duration("latency", time.Since(start)),
)
log/slog(標準ライブラリ、Go 1.21 以降)を使用します。構造化フィールドを使用し、文字列補間は使用しません。すべてのログエントリーにリクエスト ID、ユーザー ID、操作名を含めます。
よくあるアンチパターン
- 具体的な型ではなく
interface{}/anyを返す - 複雑なセットアップに
init()を使用する(テストが難しくなります) - コメントなしで
_を使ってエラーを無視する - ライフサイクルを管理せずにゴルーチンを使用する
- ロック範囲が広すぎることによるミューテックスの競合
- チャネルの誤用:単純な共有状態にはミューテックスを優先する
- 数行を超える関数で名前付き戻り値を使わない return を使用する
コメント
Markdown 対応