2026年7月1日水曜日

golang のテクニックとかイディオムとかメモ

golang のテクニックとかイディオムとかメモ

概要

忘れないようにメモです
気づいたことは随時追記します

基本的な使い方というよりかは実務で使えそうな中から上級者よりの内容を記載しています
基本的な文法というよりかは設計時に使えるテクニックなどを紹介しています

環境

  • macOS 26.5.1
  • golang 1.26.4

interface について

  • 使う目的は「差し替え可能」にするため
  • 簡単に言えばテスト用 -> 結果としてテストのためになっている
  • シンプルな interface にしなければならない
    • メソッドは 1 or 2 個くらい (必要最低限の原則)
  • 明示的な継承などは golang ではしない (できない)
    • 同じ振る舞いを持つ struct の代替になれる
    • golang が自動的に判断してくれる
type RedisClient interface {
	LRange(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd
	LPush(ctx context.Context, key string, values ...interface{}) *redis.IntCmd
	Del(ctx context.Context, keys ...string) *redis.IntCmd
}

type RedisRepository struct {
	client RedisClient
}

func NewRedisRepository(client RedisClient) *RedisRepository {
	return &RedisRepository{client: client}
}

interface の置き場所

  • 基本が使う側に定義する
    • これが不思議な感じがするが golang のルールでそうなっている
    • 使う側で振る舞いが柔軟に変更できるようになる
  • 共通インタフェースは「一番内側(ドメイン or アプリケーション層)」に置く
    • インタフェースの構成が変わっても影響しないのであればまとめて OK
    • この場合は使う側ではなく使われる側に置く

interfaceは「後から作る」

  • 最初からinterfaceを作らない
  • 使う側が必要になったときに作る
  • golang の哲学らしい

継承っぽいことをしたい場合は embedding

  • struct 内のメンバーに別の struct を値レシーバで定義すると勝手にそっちのメソッドを使えるようにしてくれる
    • structを埋め込むとメソッドが昇格(promotion)される
    • 値でもポインタでも挙動は変わる
type Controller struct {
	controller.BaseController
	memoService *service.MemoService
}

とすると Controller 側で BaseController 側のメソッドが c.CurrentUsername() という感じで使える

func (c *BaseController) CurrentUsername() string {
	if name := c.GetSession("user"); name != nil {
		if username, ok := name.(string); ok {
			return username
		}
	}
	return "anonymous"
}

New するメソッド (コンストラクタ) は基本的にポインタ型を使う

  • ポインタ型を使うとコピーではなく参照になるので具現化した大元の値まで書き換えされる
func NewController(svc *service.MemoService) *Controller {
	return &Controller{memoService: svc}
}

値レシーバとポインタレシーバ

  • 値レシーバとポインタレシーバでinterfaceの満たし方が変わる
  • どちらのレシーバを使ってメソッドを定義したかによって参照を渡すか値を渡すか決まる

以下のような場合は参照を渡さないとインタフェースを満たさないのでダメ

type I interface {
	Foo()
}

type S struct {}

func (s *S) Foo() {}

上だとエラーになる

var i I = S{}    // NG
var i I = &S{}   // OK

メソッドと関数は違う

  • 簡単に言えばメソッドは struct 配下
  • 関数は struct とは関係ない単独の関数
  • メソッドは状態(struct)に紐づく振る舞い
  • 関数は純粋な処理(副作用なしが理想)

メソッド

func (c *Controller) Login() {
	c.LogAccess("Login")
	c.RenderLayout("account/login.tpl")
}

関数
関数はレシーバの指定がない=特定の struct 配下にいない

func Replace(str string, from string, to string) string {
	return strings.Replace(str, from, to, -1)
}

lazy init はある

  • sync.OnceValue を使うのが基本
  • 使うケースはデータベースや外部リソース接続時
  • それ以外のケースでは golang では基本的には DI or DIP を使う
  • struct 内のメンバーを初期化する場合に lazy init はあまり使わない

以下のような golang では書き方はあまりしない

例: ListText が呼ばれたときに初めて textService が初期化される

type MemoService struct {
	textService  *textsvc.Service
}

func (s *MemoService) textSvc() *textsvc.Service {
	if s.textService == nil {
		s.textService = textsvc.NewService(s.repo.Text())
	}
	return s.textService
}

func (s *MemoService) ListText(ctx context.Context, username string) ([]string, error) {
	return s.textSvc().List(ctx, username)
}

generics について

  • 使うケースとしては同じ振る舞いで異なる型の引数や返り値を返す場合に使うと処理を1つにまとめられる
  • 可読性的には落ちる気がする
    • T が直感的になるまで大変
    • T が何の最終的に何の型になのか一見してわからない
  • 型安全な共通処理を作るため
  • interfaceでは表現できない「型の制約」を扱える

これを

func SumInt(a, b int) int {
	return a + b
}

func SumFloat(a, b float64) float64 {
	return a + b
}

こうできるのがジェネリクス化

func Sum[T int | float64](a, b T) T {
	return a + b
}

internal ディレクトリについて

  • モジュール外に公開されない機能
  • 同一モジュール内でのみimport可能
  • パッケージ境界を強制できるのがメリット
  • internal 配下にコードを置くと勝手にそうなってくれる
  • Python も __init__.py のスコープあり版のイメージ
  • Web アプリとか作成していると基本全部 internal 配下においても問題はないので internal が肥大化しないように注意しなければならない
    • 例えばデータベースに関する処理だけ置くとか
    • internal/database/database.go

DIP しやすい

  • golang は全体的に interface に依存させるほうがベストプラクティスになるケースが多い
  • そうなると自然と DIP となりクリーンアーキテクチャよりの設計に勝手になっていく印象
  • クリーンアーキテクチャの話になると下位から上位の意識がかなり大事になるので厳密には必ずなるわけではない
    • 書きやすいというレベル

// application(上位層)
type PaymentClient interface {
	Charge()
}

type PaymentService struct {
	client PaymentClient
}

// infrastructure(下位層)
type StripeClient struct {}

func (s *StripeClient) Charge() {}

error は特別な存在(例外がない)

  • Goには例外(try-catch)がない
  • errorは戻り値として扱う
  • 明示的に処理するのが前提
  • errors.Is / errors.As を使う
  • エラーをラップする (fmt.Errorf("%w", err))
v, err := DoSomething()
if err != nil {
    return err
}

最後に

golang は他の言語に比べてルールというかイディオム的なのが多い気がします

参考サイト

0 件のコメント:

コメントを投稿