go-common-code/readme/coding-conventions.md

8.2 KiB
Raw Permalink Blame History

Go コーディング規約

1. 一般的なガイドライン

  1. コードフォーマットは 必ず gofmt(または goimports)に従う。手で整形しない。
  2. インデントは タブ\t)を使用する(gofmtが自動で設定する)。
  3. 1行の長さは厳密な制限はないが、できれば 100 文字前後を目安にする。
  4. 1ファイルは 300〜400 行以内を目安とし、長くなりすぎる場合はファイル分割を検討する。
  5. パッケージ単位で責務を分割し、小さく・単機能な関数を心がける。
  6. エクスポート(大文字始まり)するものは最小限にし、原則として パッケージ内で完結する API を設計する。

2. コメント/ドキュメント

Go では godoc 形式のコメントが重要です。

  1. パッケージコメント: package 宣言の直前に、そのパッケージの説明を書く。

    // Package user provides user management functionalities such as
    // registration, authentication, and profile updates.
    package user
    
  2. エクスポートされた関数/メソッドには、シグネチャ名から始まるコメントを書く。

    // CreateUser creates a new user and stores it into the repository.
    func CreateUser(name string, age int) (*User, error) {
        // 実装
    }
    
  3. エクスポートされた型やフィールドにもコメントを書く。

    // User represents a user entity in the system.
    type User struct {
        // ID is a unique identifier of the user.
        ID int64
    
        // Name is the display name of the user.
        Name string
    }
    
  4. 非エクスポート(小文字始まり)のものでも、複雑な処理や分岐には行コメントで意図や前提条件を書いておく。

    // cache にヒットした場合は DB に問い合わせない
    if v, ok := cache[id]; ok {
        return v, nil
    }
    

3. 命名規則

Go は CamelCase / mixedCaps を使いますsnake_case をあまり使わない)。

  1. パッケージ名:

    • すべて小文字、_ や数字を極力避ける。
    • 短く意味のある名前にする(users より user のように単数形が好まれがち)。
    package user  // 良い例
    
  2. 変数・関数名(非エクスポート): 先頭小文字の mixedCaps

    func loadUser() (*User, error) {
        userCount := 0
        _ = userCount
        // ...
        return nil, nil
    }
    
  3. エクスポート関数・構造体: 先頭大文字の MixedCaps

    type UserService struct {
        repo Repository
    }
    
    func NewUserService(repo Repository) *UserService {
        return &UserService{repo: repo}
    }
    
  4. 定数:

    • エクスポートする定数は CamelCase
    • パッケージ内専用であれば先頭小文字。
    const DefaultTimeout = 5 * time.Second
    
    const maxRetryCount = 3
    
  5. レシーバ名:

    • 型名の1〜2文字の略を使う。
    • UserServiceusServers など。
    func (s *Server) Start() error {
        // ...
        return nil
    }
    

4. インポート/パッケージ構成

  1. インポートは 自動整形ツールgoimportsにまかせる

  2. グループ順は以下が一般的:

    1. 標準ライブラリ
    2. サードパーティ
    3. 自プロジェクト内パッケージ

    グループ間は空行で区切る。

    import (
        "context"
        "fmt"
        "net/http"
    
        "github.com/julienschmidt/httprouter"
    
        "example.com/project/internal/user"
        "example.com/project/pkg/logger"
    )
    
  3. 循環参照が起きないようにパッケージを分割する。

    • cmd/… エントリポイント(main パッケージ)
    • internal/… アプリ内部でのみ使うパッケージ
    • pkg/… 外部にも公開可能なパッケージ

5. エラーハンドリング

  1. Go のエラーは 戻り値の error で扱う。

    user, err := repo.FindByID(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("failed to find user: %w", err)
    }
    
  2. エラーはできるだけ 早めに return し、ネストを浅く保つ。

    if err != nil {
        return nil, err
    }
    // ここから成功時の処理
    
  3. panicプログラミングミスなど「復旧不能」なケースのみで使用し、通常のエラー処理には使わない。

  4. errors.Is / errors.As を使ってエラー種別を判定する。

    if errors.Is(err, sql.ErrNoRows) {
        // not found の扱い
    }
    

6. 複数ファイル間の分割時の書き方

6.1 同じパッケージ内での分割

Go では 同じディレクトリ配下の .go ファイルは同じ packageであれば、1つのパッケージとして扱われます。

user/
  user.go
  service.go
  repository.go

すべてのファイルで package user と書きます。

// user/user.go
package user

type User struct {
    ID   int64
    Name string
}
// user/service.go
package user

// Service handles user-related usecases.
type Service struct {
    repo Repository
}

func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}
// user/repository.go
package user

type Repository interface {
    FindByID(id int64) (*User, error)
    Save(user *User) error
}

同一パッケージ内であれば、インポートなしですべてのシンボルを参照できます。

6.2 main パッケージと内部パッケージの分割

cmd/app/main.go
internal/user/service.go
internal/user/repository.go
// cmd/app/main.go
package main

import (
    "log"

    "example.com/project/internal/user"
)

func main() {
    repo := user.NewInMemoryRepository()
    svc := user.NewService(repo)

    if err := svc.Run(); err != nil {
        log.Fatal(err)
    }
}
// internal/user/service.go
package user

type Service struct {
    repo Repository
}

func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}

func (s *Service) Run() error {
    // 実装
    return nil
}
// internal/user/repository.go
package user

type Repository interface {
    // ...
}

type inMemoryRepository struct {
    // ...
}

func NewInMemoryRepository() Repository {
    return &inMemoryRepository{}
}

ポイント:

  • ディレクトリとパッケージ名を一致させると分かりやすい。
  • cmd/app/main.go はできるだけ薄くし、構造体やビジネスロジックは internal/pkg/ に置く。

6.3 テストファイルの分割

テストコードは基本的に同じパッケージか package xxx_test で書きます。

// user/service_test.go
package user_test

import (
    "testing"

    "example.com/project/internal/user"
)

func TestService_Run(t *testing.T) {
    repo := user.NewInMemoryRepository()
    svc := user.NewService(repo)

    if err := svc.Run(); err != nil {
        t.Fatalf("Run() error = %v", err)
    }
}

7. gofmt / Lint / VSCode の設定

7.1 フォーマット・リンター

  1. 必須ツール

    • gofmt(標準)
    • goimports(インポートも自動整形)
  2. 推奨リンター

    • golangci-lint(多数のリンターを統合したツール)

プロジェクトルートに .golangci.yml を置く例:

run:
  timeout: 3m
  tests: true

linters:
  enable:
    - govet
    - gofmt
    - gosimple
    - staticcheck
    - unused
    - errcheck

issues:
  exclude-use-default: false

7.2 VSCode の設定例 (.vscode/settings.json)

{
  "go.useLanguageServer": true,
  "go.formatTool": "goimports",
  "gopls": {
    "staticcheck": true
  },

  "editor.formatOnSave": true,
  "[go]": {
    "editor.codeActionsOnSave": {
      "source.organizeImports": true
    }
  },

  "go.lintTool": "golangci-lint",
  "go.lintOnSave": "file",
  "go.lintFlags": [
    "run",
    "--fast"
  ],

  "files.trimTrailingWhitespace": true,
  "files.insertFinalNewline": true
}