Go言語入門:言語仕様 -Vol.51-

スポンサーリンク
Go言語入門:言語仕様 -Vol.51- 用語解説
Go言語入門:言語仕様 -Vol.51-
この記事は約13分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語の言語仕様について解説しています。

スポンサーリンク

背景

Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。

言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。

そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。

言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!

型定義(Type definitions)

型定義は、指定された型と同じ基底型(underlying type)および操作を持つ、新しい独自の型を作成し、識別子(型名)をそれに束縛します。

TypeDef = identifier [ TypeParameters ] Type .

この新しい型は定義型(defined type)と呼ばれます。定義型は、それが作成された型を含め、他のいかなる型とも異なります。

type (
	Point struct{ x, y float64 }  // Point と struct{ x, y float64 } は異なる型
	polar Point                   // polar と Point は異なる型を表す
)

type TreeNode struct {
	left, right *TreeNode
	value any
}

type Block interface {
	BlockSize() int
	Encrypt(src, dst []byte)
	Decrypt(src, dst []byte)
}

定義型にはメソッドを関連付けることができます。定義型は指定された型に束縛されたメソッドを一切継承しませんが、インターフェース型のメソッドセット、または複合型の要素のメソッドセットは変更されません。

// Mutex は Lock と Unlock の2つのメソッドを持つデータ型
type Mutex struct         { /* Mutex のフィールド */ }
func (m *Mutex) Lock()    { /* Lock の実装 */ }
func (m *Mutex) Unlock()  { /* Unlock の実装 */ }

// NewMutex は Mutex と同じ構成を持つが、メソッドセットは空
type NewMutex Mutex

// PtrMutex の基底型 *Mutex のメソッドセットは変更されないが、
// PtrMutex 自身のメソッドセットは空
type PtrMutex *Mutex

// *PrintableMutex のメソッドセットは、
// 埋め込みフィールド Mutex に束縛された Lock と Unlock メソッドを含む
type PrintableMutex struct {
	Mutex
}

// MyBlock は Block と同じメソッドセットを持つインターフェース型
type MyBlock Block

型定義は、異なるブール型、数値型、または文字列型を定義し、それらにメソッドを関連付けるために使用できます。

type TimeZone int

const (
	EST TimeZone = -(5 + iota)
	CST
	MST
	PST
)

func (tz TimeZone) String() string {
	return fmt.Sprintf("GMT%+dh", tz)
}

型定義が型パラメータを指定している場合、その型名はジェネリック型を表します。ジェネリック型は使用時にインスタンス化されなければなりません。

type List[T any] struct {
	next  *List[T]
	value T
}

型定義において、指定される型は型パラメータであってはなりません。

type T[P any] P    // 不正:P は型パラメータ

func f[P any]() {
	type L P   // 不正:P は外側の関数で宣言された型パラメータ
}

ジェネリック型にもメソッドを関連付けることができます。この場合、メソッドレシーバは、ジェネリック型定義に存在するのと同じ数の型パラメータを宣言しなければなりません。

// メソッド Len は連結リスト l の要素数を返す
func (l *List[T]) Len() int  { … }

解説

たとえ話

型定義は、料理のレシピをもとに「自分のオリジナル料理」を作ることに例えられます。

たとえば、「カレーライス」のレシピ(元の型)をベースに「我が家のスペシャルカレー」(定義型)を作ったとします。材料や基本的な作り方(基底型と操作)は同じですが、名前が違いますし、独自のアレンジ(メソッド)を加えられます。

ただし重要なのは、元のレシピに付いていたアレンジ(メソッド)は自動的には引き継がれないということです。「カレーライス」に「トッピングを追加する」というアレンジ手順があっても、「我が家のスペシャルカレー」にはそれが自動で付いてきません。使いたければ改めて自分で定義する必要があります。

コード例

package main

import "fmt"

// ══════════════════════════════════════
// 基本:型定義は新しい独自の型を作る
// ══════════════════════════════════════

type Celsius float64
type Fahrenheit float64

func main() {
	var tempC Celsius = 100.0
	var tempF Fahrenheit = 212.0

	// Celsius と Fahrenheit は両方 float64 がベースだが、別の型
	// tempC = tempF           // ❌ コンパイルエラー!
	tempC = Celsius(tempF)     // ✅ 明示的な変換が必要
	fmt.Println(tempC)
}
package main

import "fmt"

// ══════════════════════════════════════
// メソッドは継承されない
// ══════════════════════════════════════

type Base struct {
	Name string
}

// Base にメソッドを定義
func (b Base) Hello() string {
	return "こんにちは、" + b.Name + "です"
}

// Derived は Base と同じ構成を持つが、新しい型
type Derived Base

func main() {
	b := Base{Name: "ベース"}
	fmt.Println(b.Hello()) // ✅ → こんにちは、ベースです

	d := Derived{Name: "派生"}
	// fmt.Println(d.Hello()) // ❌ コンパイルエラー!Hello は継承されない
	_ = d
}
package main

import "fmt"

// ══════════════════════════════════════
// 埋め込みを使えばメソッドを「引き継ぐ」ことができる
// ══════════════════════════════════════

type Mutex struct{}

func (m *Mutex) Lock()   { fmt.Println("ロック") }
func (m *Mutex) Unlock() { fmt.Println("アンロック") }

// ── 型定義:メソッドは継承されない ──
type NewMutex Mutex

// ── 埋め込み:メソッドセットが引き継がれる ──
type PrintableMutex struct {
	Mutex // 埋め込みフィールド
}

func main() {
	nm := NewMutex{}
	// nm.Lock() // ❌ NewMutex にはメソッドがない

	pm := PrintableMutex{}
	pm.Lock()   // ✅ 埋め込みにより Mutex の Lock が使える → ロック
	pm.Unlock() // ✅ → アンロック
	_ = nm
}
package main

import "fmt"

// ══════════════════════════════════════
// インターフェース型の型定義:メソッドセットは保持される
// ══════════════════════════════════════

type Block interface {
	BlockSize() int
	Encrypt(src, dst []byte)
}

// MyBlock は Block と同じメソッドセットを持つ新しいインターフェース型
type MyBlock Block

type SimpleBlock struct{}

func (s SimpleBlock) BlockSize() int          { return 16 }
func (s SimpleBlock) Encrypt(src, dst []byte) { copy(dst, src) }

func main() {
	// SimpleBlock は Block も MyBlock も満たす
	var b Block = SimpleBlock{}
	var mb MyBlock = SimpleBlock{}

	fmt.Println(b.BlockSize())  // → 16
	fmt.Println(mb.BlockSize()) // → 16
}
package main

import "fmt"

// ══════════════════════════════════════
// 実用的な例:意味のある型名とメソッドの追加
// ══════════════════════════════════════

type TimeZone int

const (
	EST TimeZone = -(5 + iota) // -5
	CST                         // -6
	MST                         // -7
	PST                         // -8
)

// String メソッドを追加して fmt.Println での表示をカスタマイズ
func (tz TimeZone) String() string {
	return fmt.Sprintf("GMT%+dh", tz)
}

func main() {
	fmt.Println(EST) // → GMT-5h
	fmt.Println(PST) // → GMT-8h
}
package main

import "fmt"

// ══════════════════════════════════════
// ジェネリック型定義(Go 1.18 以降)
// ══════════════════════════════════════

// T は型パラメータ。使用時に具体的な型を指定する
type List[T any] struct {
	next  *List[T]
	value T
}

// ジェネリック型のメソッド:レシーバに同じ数の型パラメータを書く
func (l *List[T]) Push(val T) *List[T] {
	return &List[T]{next: l, value: val}
}

func (l *List[T]) Values() []T {
	var result []T
	for node := l; node != nil; node = node.next {
		result = append(result, node.value)
	}
	return result
}

func main() {
	// List[int] としてインスタンス化
	var head *List[int]
	head = head.Push(1)
	head = head.Push(2)
	head = head.Push(3)

	fmt.Println(head.Values()) // → [3 2 1]

	// List[string] は List[int] とは別の型
	var strHead *List[string]
	strHead = strHead.Push("hello")
	fmt.Println(strHead.Values()) // → [hello]
}
package main

// ══════════════════════════════════════
// 型定義で型パラメータそのものは指定できない
// ══════════════════════════════════════

// ❌ 型パラメータを直接使った型定義は不正
// type T[P any] P       // コンパイルエラー

// ❌ 関数内でも同様
// func f[P any]() {
// 	type L P            // コンパイルエラー
// }

// ✅ 型パラメータは「構造の中で使う」のが正しい
type Wrapper[T any] struct {
	Value T
}

よくある間違い・注意点

1. 「メソッドは継承されない」は最もつまずきやすいポイント

他の言語のクラス継承に慣れている人は、type NewMutex MutexMutex のメソッドが使えると期待しがちです。Goではメソッドは継承されません。メソッドを引き継ぎたい場合は、型定義ではなく構造体の埋め込みを使います。

// ❌ 型定義 → メソッドなし
type NewMutex Mutex

// ✅ 埋め込み → メソッドが昇格される
type PrintableMutex struct {
	Mutex
}

2. 元の型と定義型は「異なる型」であることを忘れない

type Point struct{ x, y float64 }
type polar Point

// Point と polar は異なる型
// polar 型の値を Point 型の変数に代入するには変換が必要
p := polar{x: 1, y: 2}
pt := Point(p) // 明示的な変換

3. ポインタ型の型定義にも注意

type PtrMutex *Mutex
// PtrMutex 自身のメソッドセットは空!
// *Mutex のメソッド(Lock, Unlock)は PtrMutex からは呼べない

ポインタ型を型定義しても元のポインタ型のメソッドは使えません。ポインタ型の型定義が必要なケースはほとんどないため、通常は避けるのが無難です。

4. ジェネリック型のメソッドレシーバには型パラメータが必須

type List[T any] struct { value T }

// ✅ レシーバに T を書く
func (l *List[T]) Get() T { return l.value }

// ❌ 型パラメータを省略するとエラー
// func (l *List) Get() { }

まとめ

  • 型定義(type A B)は、元の型と同じ基底型を持つがまったく別の新しい型を作る。
  • 定義型にはメソッドを追加できるが、元の型のメソッドは継承されない
  • メソッドを引き継ぎたい場合は、型定義ではなく構造体の埋め込みを使う。
  • インターフェース型を型定義した場合、メソッドセットは保持される。
  • ジェネリック型は型パラメータ付きで定義し、使用時にインスタンス化する。メソッドレシーバにも同じ数の型パラメータが必要。
  • 型パラメータそのものを型定義の対象にすることはできない。

型定義はGoの型システムの中核であり、「メソッドの追加」「型安全性の確保」「意味のある名前付け」の3つの目的で使われます。特に「メソッドが継承されない」というルールは、Goが「継承より合成」を重視する設計思想を体現した部分です。しっかり理解しておくと、Goらしい設計ができるようになりますよ!

おわりに 

本日は、Go言語の言語仕様について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました