0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Golangの重要そうな13の仕様と動作確認

Posted at

背景:なぜGo仕様を読むことにしたのか?

Golangを書くことはあるのですが、仕様についてガッツリと確認したことがなかったので、Golangの仕様を読みました。

特に気をつけたほうが良さそうな箇所を13個ピックアップしました。

一部箇所で概念実証(PoC)を行いました。Golangのバージョンは1.24.2 を使用しています。

検証ポイント:重要そうな13の仕様と動作確認

🏗️ 構造・型システムに関する項目(Struct / 型同一性)

1. Struct Promote Methods with Embedded Types

重要だと感じた理由:埋め込み構造体のメソッドが昇格する挙動は、設計ミスやnil参照パニックを引き起こす可能性があり、またこれを理解することで重複するようなコードを避けることができるため。

Goでは、構造体に別の構造体を埋め込むことができます。
加えて、埋め込まれた構造体のメソッドは、埋め込んだ構造体のメソッドとしても呼び出すことができます。

If S contains an embedded field T, the method sets of S and *S both include promoted methods with receiver T. The method set of *S also includes promoted methods with receiver *T.
If S contains an embedded field *T, the method sets of S and *S both include promoted methods with receiver T or *T.

PoC

下記のコードで上記の内容を確認しました。
特に興味深いのは、Structに埋め込まれたT構造体のメソッドは、S構造体のメソッドとしても呼び出せるが、*T構造体のメソッドは呼び出せないという点についてコンパイラはエラーを出さないが、実行時にnilポインタ参照エラーを起こすという点です。

package main

import "fmt"

type T struct{}
func (T) Foo() {
  fmt.Println("T.Foo()")
}
func (*T) Bar() {
  fmt.Println("*T.Bar()")
}

type S struct {
  T
}

type SDash struct {
  *T
}

func main() {
    var s S
    s.Bar()
    s.Foo()

    var sdash SDash
    sdash.Bar()
    sdash.Foo()
}
❯ go run main.go
*T.Bar()
T.Foo()
*T.Bar()
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4809b1]

goroutine 1 [running]:
main.main()
        ./hajimeteno-golang-specification/main.go:28 +0xd1
exit status 2

2. Struct can Reference Itself whose type is pointer

重要だと感じた理由:構造体がポインタを使用して自己参照することができるため、データ構造の設計において柔軟性があるため。

A struct type T may not contain a field of type T, or of a type containing T as a component, directly or indirectly, if those containing types are only array or struct types.

上記の文でT自身を参照することはできないと書いてありますが、ポインタであれば自己参照が可能であることがわかります。

PoC

例えば、連想配列のような構造体を作ることができます。

package main

import "fmt"

type Node struct {
    Value int
    Next  *Node
}

func main() {
    n1 := &Node{Value: 1}
    n2 := &Node{Value: 2}
    n3 := &Node{Value: 3}

    n1.Next = n2
    n2.Next = n3

    fmt.Println(n1.Value) // 1
    fmt.Println(n1.Next.Value) // 2
    fmt.Println(n1.Next.Next.Value) // 3

    selfLoop := &Node{Value: 1}
    selfLoop.Next = selfLoop

    fmt.Println(selfLoop.Value) // 1
    fmt.Println(selfLoop.Next.Value) // 1
    fmt.Println(selfLoop.Next.Next.Value) // 1
    fmt.Println(selfLoop.Next.Next.Next.Value) // 1
}

3. Underlying Type, Type Identity, and Assignability

重要だと感じた理由:型の同一性と代入可能性は、型安全性を確保するために重要な概念であり、特にGenericsを使用する際に理解しておく必要があるため。

A named type is always different from any other type. Otherwise, two types are identical if their underlying type literals are structurally equivalent; that is, they have the same literal structure and corresponding components have identical types.

型が"identical"であるとは、型が異なるが、基礎となる型"underlying type"が同じであることを意味します。

identicalな場合は"assignable"な状態になるため、"underlying type"を知っておく必要があると思います。

A value x of type V is assignable to a variable of type T ("x is assignable to T") if one of the following conditions applies:

  • V and T have identical underlying types but are not type parameters and at least one of V or T is not a named type.

"underlying type"は、型の基礎となる型のことを指し、定義としては次のようになります。

Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its declaration. For a type parameter that is the underlying type of its type constraint, which is always an interface.

PoC

以下の例で、型の同一性と代入可能性を確認できます。

package main

type A int
type B int

func calculateGeneral[T ~int](a T) A {
  return A(a * 2)
}

type Person struct {
  Name string
  Age  int
}

type Teacher struct {
  Name string
  Age  int
}


func main() {
  var a A = 5
  resultA := calculateGeneral(a)
  println(resultA) // Output: 10

  var b B = 10
  // a = b // Compile error: cannot use b (variable of type B) as A value in assignment
  resultB := calculateGeneral(b)
  println(resultB) // Output: 20

  teacher := Teacher{Name: "John", Age: 30}
  // var person Person = teacher // Compile error: cannot use teacher (variable of type Teacher) as Person value in assignment: Teacher does not implement Person
  person := Person(teacher) // Implicit conversion from Teacher to Person

  println(person.Name) // Output: John
  println(person.Age)  // Output: 30
}

これにより、以下のことがわかります:

  1. 名前付き型は異なるため、Undelying Typeが同じであっても直接代入はできない。
  2. 明示的な型変換を行うことで、Undelying Typeが同じであれば代入できる。
  3. Genericな関数を使用することで、Undelying Typeが同じであれば代入できる。
  4. 構造体のフィールドに対しても、Undelying Typeが同じであれば明示的に型変換を行うことで代入できる。

4. Selector

重要だと感じた理由:SelectorはGoの型システムにおいて重要な概念であり、特に埋め込み構造体やポインタを使用する際に注意が必要なため。

A selector f may denote a field or method f of a type T, or it may refer to a field or method f of a nested embedded field of T. The number of embedded fields traversed to reach f is called its depth in T. The depth of a field or method f declared in T is zero. The depth of a field or method f declared in an embedded field A in T is the depth of f in A plus one.

  1. For a value x of type T or *T where T is not a pointer or interface type, x.f denotes the field or method at the shallowest depth in T where there is such an f. If there is not exactly one f with shallowest depth, the selector expression is illegal.
  2. For a value x of type I where I is an interface type, x.f denotes the actual method with name f of the dynamic value of x. If there is no method with name f in the method set of I, the selector expression is illegal.
  3. As an exception, if the type of x is a defined pointer type and (*x).f is a valid selector expression denoting a field (but not a method), x.f is shorthand for (*x).f.
  4. In all other cases, x.f is illegal.
  5. If x is of pointer type and has the value nil and x.f denotes a struct field, assigning to or evaluating x.f causes a run-time panic.
  6. If x is of interface type and has the value nil, calling or evaluating the method x.f causes a run-time panic.

Selectorについては、上の6つのルールさえ覚えておけば、あとは実行時にpanicが起きるかどうかを確認すれば良い状態になります。

PoC

以下はStructに対してのSelectorの例です(Interfaceについては比較的簡単なので省略します)。

package main

import "fmt"

// 基本構造体
type Base struct {
  Name string
}

// Baseのメソッド
func (b Base) GetName() string {
  return b.Name
}

func (b *Base) SetName(name string) {
  b.Name = name
}

// 埋め込みを使った構造体
type Derived struct {
  Base  // 埋め込み(深さ1)
  Age int
}

// さらに埋め込みを使った構造体
type SuperDerived struct {
  Derived  // 埋め込み(Base は深さ2)
  Job string
}

// 同じ名前のフィールドを持つ構造体
type Conflicting struct {
  Base1 Base // 名前の競合
  Base2 Base // 名前の競合
}

// インターフェース
type Named interface {
  GetName() string
}

func main() {
  // 1. 基本的なセレクタの使用
  John := "John"
  base := Base{Name: John}
  fmt.Println(base.Name)       // フィールドへのアクセス
  fmt.Println(base.GetName())  // メソッドへのアクセス
  fmt.Println(&John)

  // 2. ポインタ経由のセレクタ
  basePtr := &Base{}
  fmt.Println(basePtr.Name)      // (*basePtr).Name の省略形
  fmt.Println(&((*basePtr).Name))
  fmt.Println(&(basePtr.Name))
  basePtr.Name = "Doe"
  fmt.Println(basePtr.GetName()) // 値レシーバーのメソッドもポインタから呼び出せる
  fmt.Println(&(basePtr.Name))
  gottenValue := basePtr.GetName()
  fmt.Println(&(gottenValue)) // 変数のアドレスを取得

  // 3. 埋め込みとセレクタ
  derived := Derived{
    Base: Base{Name: "Bob"},
    Age:  30,
  }
  fmt.Println(derived.Name)      // 埋め込まれたBaseのフィールドにアクセス
  fmt.Println(derived.GetName()) // 埋め込まれたBaseのメソッドにアクセス

  // 4. 多段の埋め込みとセレクタ
  superDerived := SuperDerived{
    Derived: derived,
    Job:     "Engineer",
  }
  fmt.Println(superDerived.Name)      // 2段階の埋め込みを通してアクセス
  fmt.Println(superDerived.GetName()) // 2段階の埋め込みを通してメソッドにアクセス
  fmt.Println(superDerived.Age)       // 直接埋め込まれたDerivedのフィールドにアクセス

  // 5. 名前の競合 -> Promotionの失敗
  // conflictingFailed := Conflicting{
  //   Base1: Base{Name: "Original"},
  //   Base2: Base{Name: "Conflict"},
  // }
  // fmt.Println(conflictingFailed.Name) // Compile Error: conflictingFailed.Name undefined
  
  // 明示的に指定する必要がある
  conflicting := Conflicting{
    Base1: Base{Name: "Original"},
    Base2: Base{Name: "Conflict"},
  }
  fmt.Println(conflicting.Base1.Name)  // Base.Name を明示
  fmt.Println(conflicting.Base2.Name) // Other.Name を明示

  // 6. nilポインタとセレクタ(実行時パニック)
  // var nilPtr *Base = nil
  // fmt.Println(nilPtr.Name) // パニック: ランタイムエラー

  // 7. nilインターフェースとセレクタ(実行時パニック)
  // var nilInterface Named = nil
  // fmt.Println(nilInterface.GetName()) // パニック: ランタイムエラー
}
❯ go run main.go
John
John
0xc00012a130

0xc00012a160
0xc00012a160
Doe
0xc00012a160
0xc00012a180
Bob
Bob
Bob
Bob
30
Original
Conflict

🚦 並行処理・同期処理

5. Go Statements

重要だと感じた理由:どこで非同期処理が記述されるか明確となり、Goの並行処理の基本的な概念を理解できるため。

A "go" statement starts the execution of a function call as an independent concurrent thread of control, or goroutine, within the same address space.

TypescriptやPythonなどの言語では、asyncawaitを使用して非同期処理を行いますが、Goではgoを使用して非同期処理を行います。また、goは関数の呼び出し側で記述されるため、関数の定義側でgoを記述する必要はありません。

6. Channel

重要だと感じた理由:Goの並行処理において、チャネルは重要な役割を果たしており、特にデータの送受信や同期処理に関する理解が必要なため。

非同期処理を書く際に利用することがあるチャネルの仕様について確認します。

A channel provides a mechanism for concurrently executing functions to communicate by sending and receiving values of a specified element type.

The <- operator associates with the leftmost chan possible

The capacity, in number of elements, sets the size of the buffer in the channel. If the capacity is zero or absent, the channel is unbuffered and communication succeeds only when both a sender and receiver are ready. Otherwise, the channel is buffered and communication succeeds without blocking if the buffer is not full (sends) or not empty (receives). A nil channel is never ready for communication.

PoC

以下のコードで、Channelの概念を確認できます。

package main

import (
  "fmt"
  "time"
)

// 送信専用チャネルを引数に取る関数
func producer(ch chan<- int) {
  for i := 0; i < 5; i++ {
    ch <- i  // チャネルに値を送信
    fmt.Printf("送信: %d\n", i)
  }
  close(ch)  // チャネルを閉じる
}

// 受信専用チャネルを引数に取る関数
func consumer(ch <-chan int) {
  // チャネルからの受信(チャネルが閉じられるまで)
  for {
    val, ok := <-ch
    if !ok {
      // チャネルが閉じられた場合
      fmt.Println("チャネルは閉じられました")
      return
    }
    fmt.Printf("受信: %d\n", val)
    time.Sleep(100 * time.Millisecond) // 受信側が遅いケース
  }
}

func main() {
  // バッファなしチャネルの例
  fmt.Println("=== バッファなしチャネル ===")
  unbuffered := make(chan int)

  // NOTE: 順番を変えると、mainプロセスがDeadLockになるため注意
  go producer(unbuffered)
  consumer(unbuffered)

  // バッファありチャネルの例
  fmt.Println("\n=== バッファありチャネル(容量2) ===")
  buffered := make(chan int, 2)
  
  // バッファ内にどれだけ値が格納されているか確認
  fmt.Printf("バッファ使用量: %d, 容量: %d\n", len(buffered), cap(buffered))
  
  // バッファが満杯になるまでブロックしない
  buffered <- 10
  fmt.Printf("10を送信後 - バッファ使用量: %d\n", len(buffered))
  
  buffered <- 20
  fmt.Printf("20を送信後 - バッファ使用量: %d\n", len(buffered))
  
  // バッファから値を取り出す
  val1 := <-buffered
  fmt.Printf("受信: %d, バッファ使用量: %d\n", val1, len(buffered))
  
  val2 := <-buffered
  fmt.Printf("受信: %d, バッファ使用量: %d\n", val2, len(buffered))
}

❯ go run main.go
=== バッファなしチャネル ===
送信: 0
受信: 0
受信: 1
送信: 1
送信: 2
受信: 2
受信: 3
送信: 3
受信: 4
送信: 4
チャネルは閉じられました

=== バッファありチャネル(容量2) ===
バッファ使用量: 0, 容量: 2
10を送信後 - バッファ使用量: 1
20を送信後 - バッファ使用量: 2
受信: 10, バッファ使用量: 1
受信: 20, バッファ使用量: 0

🔄 制御フローと関数まわり

7. Type Switch

重要だと感じた理由:型スイッチは、Goの型システムにおいて非常に重要な概念であり、特にインターフェースを使用したダウンキャストや型アサーションに関連しているため。

xinterface{}型として扱う場合、xの実際の型を判別するためx.(type)という構文とswitch文を組み合わすことで、型スイッチを使用して異なる型に基づいて異なる処理を行うことができます。

こちらについては公式ドキュメントで十分だと感じたためPoCは省略します。

8. Defer

重要だと感じた理由:deferは、関数の終了時に特定の処理を実行するための重要な機能であり、FIFOの動作を理解することで、リソースの解放やエラーハンドリングに役立つため。

Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred.

deferの呼び出しはFIFOで、Returnが実行される前に呼び出されます。

PoC

以下のコードで、deferのFIFOの動作を確認できます。

package main

import (
  "fmt"
)

func submain() string {
  defer func() {
    fmt.Println("Deferred function executed 001")
  }()
  defer func() {
    fmt.Println("Deferred function executed 002")
  }()
  return "Hello, World!"
}

func main() {
  submain_result := submain()
  fmt.Println(submain_result)
}
❯ go run main.go
Deferred function executed 002
Deferred function executed 001
Hello, World!

🧮 組み込み関数と演算系

9. Min Max

重要だと感じた理由:型が異なるものは書けないことを再認識する上で良い例であると感じたため。

c := max(1, 2.0, 10)のような形でnumberの最大値を取得することができます。

ここで注意が必要なのは、引数に取れる値の型は同じでなければならないという点です。上の例は異なる型に見えますが、引数を変数に渡してからmax関数を利用するのと利用しないとで、出力内容が変化します。

詳細については下記のPoCを参照してください。

⚙️ 初期化・依存・変数スコープまわり

10. For Clause

重要だと感じた理由:Forループの初期化部分で変数を宣言する際の挙動が変わり昔のコードと現在のコードで挙動が異なるため。

Each iteration has its own separate declared variable (or variables) [Go 1.22]. The variable used by the first iteration is declared by the init statement. The variable used by each subsequent iteration is declared implicitly before executing the post statement and initialized to the value of the previous iteration's variable at that moment.

Go 1.22より前では、Forループの中で共有した一つの値を使用していましたが、Go 1.22以降では、各イテレーションごとに新しい変数が作成されるようになりました。

こちらも公式ドキュメントで十分だと感じたためPoCは省略します。

11. Allocation

重要だと感じた理由:new関数の挙動を理解することで、メモリ管理やポインタの扱いに関する理解が深まるため。

The built-in function new takes a type T, allocates storage for a variable of that type at run time, and returns a value of type *T pointing to it. The variable is initialized as described in the section on initial values.

つまり、new(T)はポインタを返すということです。

PoC

以下で、newの動作を確認できます。

package main

import (
  "fmt"
)

type Person struct {
  name string
  age  int
}

func main() {
  new_int := new(int)
  *new_int = 42
  fmt.Println(*new_int)
  normal_int := 42
  fmt.Println(&normal_int == new_int)

  new_person := new(Person)
  new_person.name = "John"
  new_person.age = 30  
  normal_person := Person{
    name: "John",
    age:  30,
  }
  fmt.Println(new_person == &normal_person)
}
❯ go run main.go
42
false
false

12. Package Initialization

重要だと感じた理由:パッケージの初期化順序が不定である状況が存在し、初期化はかなり複雑なものであるため。

Dependency analysis does not rely on the actual values of the variables, only on lexical references to them in the source, analyzed transitively. For instance, if a variable x's initialization expression refers to a function whose body refers to variable y then x depends on y. Specifically:

  • A reference to a variable or function is an identifier denoting that variable or function.
  • A reference to a method m is a method value or method expression of the form t.m, where the (static) type of t is not an interface type, and the method m is in the method set of t. It is immaterial whether the resulting function value t.m is invoked.
  • A variable, function, or method x depends on a variable y if x's initialization expression or body (for functions and methods) contains a reference to y or to a function or method that depends on y.

こちらの内容から依存関係の分析は、実際の値によらずにソースコードの字句を分析することに依存していることがわかり、それは推移するとあります。

そのためGolangの初期化はコードの上から下へと行われるのではなく、依存関係に基づいて行われることになります。

上記については比較的理解がしやすいのですが、Interfaceに関する記述の下記の部分は注意が必要です。

Dependency analysis is performed per package; only references referring to variables, functions, and (non-interface) methods declared in the current package are considered. If other, hidden, data dependencies exists between variables, the initialization order between those variables is unspecified.

上で記述されている通り、Interfaceに関する依存関係は考慮されないため、Interfaceを使用している場合は注意が必要です。実際に公式ドキュメントの記述にあるコードをもとにPoCを確認します。

PoC

PoC 1 : sideEffectがある場合

package main

import "fmt"

// 隠れた依存関係のある変数
type I interface { ab() []int }
type T struct{}

func (T) ab() []int { return []int{hidden_a, hidden_b} }

var hidden_x = I(T{}).ab()  // hidden_xには、hidden_aとhidden_bへの検出されない隠れた依存関係がある
var _ = sideEffect()
var hidden_a = hidden_b
var hidden_b = 42

func sideEffect() bool {
  fmt.Println("sideEffect()が呼び出されました")
  return true
}

func main() {
  fmt.Println("\n=== 隠れた依存関係 ===")
  fmt.Printf("hidden_x = %v\n", hidden_x)
  fmt.Printf("hidden_a = %d\n", hidden_a)
  fmt.Printf("hidden_b = %d\n", hidden_b)
  fmt.Println("注: hidden_xの初期化タイミングは不定")
}

この実行結果は下記のとおりになります

❯ go run main.go
sideEffect()が呼び出されました

=== 隠れた依存関係 ===
hidden_x = [0 42]
hidden_a = 42
hidden_b = 42
注: hidden_xの初期化タイミングは不定
PoC 2 : sideEffectがない場合

package main

import "fmt"

// 隠れた依存関係のある変数
type I interface { ab() []int }
type T struct{}

func (T) ab() []int { return []int{hidden_a, hidden_b} }

var hidden_x = I(T{}).ab()  // hidden_xには、hidden_aとhidden_bへの検出されない隠れた依存関係がある
// var _ = sideEffect()
var hidden_a = hidden_b
var hidden_b = 42

// func sideEffect() bool {
//   fmt.Println("sideEffect()が呼び出されました")
//   return true
// }

func main() {
  fmt.Println("\n=== 隠れた依存関係 ===")
  fmt.Printf("hidden_x = %v\n", hidden_x)
  fmt.Printf("hidden_a = %d\n", hidden_a)
  fmt.Printf("hidden_b = %d\n", hidden_b)
  fmt.Println("注: hidden_xの初期化タイミングは不定")
}

❯ go run main.go

=== 隠れた依存関係 ===
hidden_x = [42 42]
hidden_a = 42
hidden_b = 42
注: hidden_xの初期化タイミングは不定

PoC まとめ

上記の2つのコードはsideEffectの有無によって、hidden_xの初期化タイミングが変わることを示しています。これは、interfaceを使用しているため依存関係の分析が明確でなく挙動が不定になってしまうことを示しています。

そのため、Packageの初期化のコードには、interfaceを使用しないようにしたほうが良いのではと考えます。

13. Program Initialization

重要だと感じた理由:プログラムの初期化は、Goの実行時において非常に重要な部分であり、特にinit関数やパッケージの初期化に関連するため。

Package initialization—variable initialization and the invocation of init functions—happens in a single goroutine, sequentially, one package at a time. An init function may launch other goroutines, which can run concurrently with the initialization code. However, initialization always sequences the init functions: it will not invoke the next one until the previous one has returned.

つまり、init関数は一つのgoroutineで実行され、順次実行されるということです。

これは特にPoCはしないですが、先程あったPackage Initializationの内容と合わせて考えると、init関数の中でinterfaceを使用することは避けたほうが良いということになります。

Question

1. Golangの初期化について

上記の項目は自分にとってはどれも重要でしたが、特に"Package Initialization"の内容については初期化の順序が不定であることを示しているため、注意が必要だと感じました。

そこで、以下のような質問を考えました。

  • 初期化でどこまでの責任をもたせるのが良いのか?
    • 特に init関数で行う内容について
    • また、何をグローバル変数に持たせるべきで、何を持たせないべきか?

Answer (ChatGPT o3による回答をもとに作成)

1. なぜ慎重さが要るのか
  • 依存解析は「字面」でしか行われない
    パッケージ変数は「まだ初期化されていない変数を参照していないか」で順番が決まり、処理系は インターフェース経由や隠れた副作用 を見落とすことがあります (Go)。
  • init は実行順が読みにくい
    ファイル名 → 依存解析 → 同じパッケージ内で複数存在可… と複雑なため、「いつ実行されるか」がコードリーディングだけでは追いづらい (Henrique Vicente)。
  • 副作用はテストと再利用性を壊す
    init で発生する副作用は インポートしただけで走る ので、テストの成否がパッケージ依存になりがち (Stack Overflow)。
2. init 関数に許してよい“責任”の上限
✔︎ 入れても良い ✘ 避けるべき
極軽量で失敗しない 代入
(定数テーブル・正規表現の MustCompile など)
I/O・ネットワーク・環境変数読込・ファイルオープン
ライブラリ内レジストリへの 登録だけ
image/pngdatabase/sql ドライバ等)
エラー処理を伴う初期化(失敗時 panic
ランタイム全体に 1 回だけ効かせる設定
rand.Seedruntime.GOMAXPROCS 程度)
goroutine 起動・重い計算・監視ループ
sync.Once で保護した 遅延 初期化のトリガ インターフェース経由の依存解決(隠れ依存を生む)

ルール:init では「失敗しない・順序依存がない・副作用が小さい」ものに留める。必要以上に詰め込むと、調査しづらいバグやテスト不能のコードになる。

3. グローバル変数を「持たせる/持たせない」の判断軸
3.1 持たせても良いもの(事実上の定数)
種類 注意点
定数・イミュータブル値 バージョン文字列、数学定数、エラーマップ 再代入禁止 (const or unexported)
プリコンパイル済みオブジェクト var matchFoo = regexp.MustCompile(pattern) MustCompile が panic しない pattern であること
フォールバック用ポインタ ロガーのダミー (log.Logger{}) nilであることを避けるためのもので、その後 main で 1 回だけ正しい値に差し替えられるもの
3.2 持たせないほうが良いもの
種類 なぜ避けるか
ミューテートされる状態(キャッシュ、カウンタ) 共有 mutable state は競合とテスト困難を招く
外部リソース(DB 接続、ファイルハンドラ、HTTP クライアント) ライフサイクル管理・エラー処理・再接続が隠れがち
設定・フラグ値 テストごとに差し替えたくなる → DI で渡す方が安全

判断のコツ: 「プログラム全体で不変か?」と「クリーンアップ不要か?」の 2 つを満たせばグローバル可。どちらかに“不変でない/クリーンアップ要”が絡むなら、明示的に渡す(構造体フィールド・関数引数・context)設計へ。

4. チェックリスト
  1. init に入れようとしている処理は本当に失敗しないか?
    → Yes: OK / No: 外に出す
  2. 順序に依存しないか?他パッケージの値を読む?
    → Yes: 外に出す
  3. 重い CPU/I/O がないか?
    → Yes: 遅延ロード+sync.Once (sync.Once を中で使えば多重呼び出しも防げる (Cristian Curteanu))
  4. グローバル変数は不変か?クリーンアップ不要か?
    → No: 構造体/関数引数で渡す
  5. テストで差し替えられるか?
    → 差替不可なら DI パターンを検討
5. まとめ
  • init は“最後の手段”:失敗しない軽量処理やテーブル生成、レジストリ登録に限定。
  • グローバル変数は“事実上の定数”に限って許可。動的な依存は main–>Set or DI で解決。
  • テストと可読性を最優先に設計すれば、「初期化順でハマるバグ」「隠れた副作用」はほぼ防げる。

このガイドラインを守れば、Go の明快な依存モデルを壊さずに安全で予測可能な初期化を実現できます。

余談

Golangの仕様を今回大きく確認をしましたが、デファクトがどうなっているかなどを理解するには別の機会を設けたいと考えました。

特に以下のようなドキュメントは確認しようと思います:

  1. Effective Go - The Go Programming Language
  2. A Guide to the Go Garbage Collector - The Go Programming Language
  3. The Go Memory Model - The Go Programming Language
  4. Standard library - Go Packages
    1. sync package - sync - Go Packages
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?