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

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

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

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

スポンサーリンク

背景

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

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

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

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

General interfaces(一般的なインターフェース)

最も一般的な形式では、インターフェース要素は任意の型項T、基底型Tを指定する形式~Tの項、または項の和集合t1|t2|…|tnである場合もあります[Go 1.18]。メソッド仕様と共に、これらの要素は次のようにインターフェースの型集合の正確な定義を可能にします:

  • 空のインターフェースの型集合は、すべての非インターフェース型の集合です。
  • 空でないインターフェースの型集合は、そのインターフェース要素の型集合の交差です。
  • メソッド仕様の型集合は、メソッドセットにそのメソッドを含むすべての非インターフェース型の集合です。
  • 非インターフェース型項の型集合は、その型だけで構成される集合です。
  • 形式~Tの項の型集合は、基底型がTであるすべての型の集合です。
  • 項の和集合t1|t2|…|tnの型集合は、項の型集合の和集合です。

「すべての非インターフェース型の集合」という量化は、手元のプログラムで宣言されたすべての(非インターフェース)型だけでなく、すべての可能なプログラムにおけるすべての可能な型を指すため、無限です。同様に、特定のメソッドを実装するすべての非インターフェース型の集合が与えられた場合、それらの型のメソッドセットの交差には、手元のプログラムのすべての型が常にそのメソッドを別のメソッドと組み合わせている場合でも、正確にそのメソッドが含まれます。

構造上、インターフェースの型集合にインターフェース型が含まれることはありません。

// int型のみを表すインターフェース
interface {
	int
}

// 基底型がintであるすべての型を表すインターフェース
interface {
	~int
}

// 基底型がintであり、Stringメソッドを実装するすべての型を表すインターフェース
interface {
	~int
	String() string
}

// 空の型集合を表すインターフェース: intかつstringである型は存在しない
interface {
	int
	string
}

形式~Tの項では、Tの基底型はT自身でなければならず、Tはインターフェースであってはなりません。

type MyInt int

interface {
	~[]byte  // []byteの基底型はそれ自身
	~MyInt   // 不正: MyIntの基底型はMyIntではない
	~error   // 不正: errorはインターフェース
}

和集合要素は型集合の和集合を示します:

// Floatインターフェースはすべての浮動小数点型を表す
// (基底型がfloat32またはfloat64である名前付き型を含む)
type Float interface {
	~float32 | ~float64
}

形式Tまたは~Tの項における型Tは型パラメータであってはならず、すべての非インターフェース項の型集合はペアごとに互いに素でなければなりません(型集合のペアごとの交差は空でなければなりません)。型パラメータPが与えられた場合:

interface {
	P                // 不正: Pは型パラメータ
	int | ~P         // 不正: Pは型パラメータ
	~int | MyInt     // 不正: ~intとMyIntの型集合は互いに素ではない(~intはMyIntを含む)
	float32 | Float  // 型集合は重複するがFloatはインターフェース
}

実装制限: 和集合(2つ以上の項を持つ)には、事前宣言された識別子comparable、メソッドを指定するインターフェース、またはcomparableやメソッドを指定するインターフェースを埋め込むことはできません。

基本的でないインターフェースは、型制約として、または制約として使用される他のインターフェースの要素としてのみ使用できます。値や変数の型、または他の非インターフェース型のコンポーネントとして使用することはできません。

var x Float                     // 不正: Floatは基本的なインターフェースではない

var x interface{} = Float(nil)  // 不正

type Floatish struct {
	f Float                 // 不正
}

インターフェース型Tは、直接的または間接的に、Tである、Tを含む、またはTを埋め込む型要素を埋め込むことはできません。

// 不正: Badは自分自身を埋め込めない
type Bad interface {
	Bad
}

// 不正: Bad1はBad2を使用して自分自身を埋め込めない
type Bad1 interface {
	Bad2
}
type Bad2 interface {
	Bad1
}

// 不正: Bad3はBad3を含む和集合を埋め込めない
type Bad3 interface {
	~int | ~string | Bad3
}

// 不正: Bad4はBad4を要素型として含む配列を埋め込めない
type Bad4 interface {
	[10]Bad4
}

解説

一般的なインターフェース(Go 1.18+)とは?

Go 1.18で導入された一般的なインターフェースは、メソッドだけでなく、型そのものをインターフェースの要素として定義できる機能です。主にジェネリクスの型制約として使われます。

たとえ話: 一般的なインターフェースは「詳細な応募資格」のようなものです。従来は「〇〇ができる人」(メソッド)だけでしたが、今は「20歳以上の人」(型)や「学生または社会人」(型の和集合)といった条件も指定できます。


1. 型項の基本

特定の型を指定

package main

import "fmt"

// int型のみを受け入れるインターフェース
type IntOnly interface {
    int
}

// ジェネリック関数で使用
func Double[T IntOnly](x T) T {
    return x * 2
}

func main() {
    result := Double(21)
    fmt.Println(result) // 42
    
    // これはエラー
    // result2 := Double(3.14)  // float64は使えない
}

複数の型を許可(和集合)

package main

import "fmt"

// intまたはfloat64を受け入れる
type Number interface {
    int | float64
}

func Add[T Number](a, b T) T {
    return a + b
}

func main() {
    // intで使える
    fmt.Println(Add(10, 20))  // 30
    
    // float64でも使える
    fmt.Println(Add(3.14, 2.86))  // 6.0
}

2. 基底型制約(~演算子)

~Tは「基底型がTであるすべての型」を表します。

~intの使用

package main

import "fmt"

type MyInt int
type YourInt int

// 基底型がintであるすべての型
type Integer interface {
    ~int
}

func Increment[T Integer](x T) T {
    return x + 1
}

func main() {
    // 標準のint
    fmt.Println(Increment(42))  // 43
    
    // カスタム型MyInt
    var m MyInt = 10
    fmt.Println(Increment(m))   // 11
    
    // カスタム型YourInt
    var y YourInt = 20
    fmt.Println(Increment(y))   // 21
}

~stringの使用

package main

import "fmt"

type UserID string
type ProductID string

// 基底型がstringであるすべての型
type StringLike interface {
    ~string
}

func ToUpper[T StringLike](s T) T {
    return T(string(s) + "!")
}

func main() {
    // 標準のstring
    fmt.Println(ToUpper("hello"))  // hello!
    
    // カスタム型
    var uid UserID = "user123"
    fmt.Println(ToUpper(uid))  // user123!
}

3. メソッドと型制約の組み合わせ

型制約とメソッド要求を組み合わせられます。

基本的な組み合わせ

package main

import "fmt"

// 基底型がintで、Stringメソッドを持つ型
type StringableInt interface {
    ~int
    String() string
}

type MyInt int

func (m MyInt) String() string {
    return fmt.Sprintf("MyInt(%d)", m)
}

func Print[T StringableInt](x T) {
    fmt.Println(x.String())
}

func main() {
    var m MyInt = 42
    Print(m)  // MyInt(42)
}

実用的な例

package main

import "fmt"

// 数値型で、Compareメソッドを持つ
type Ordered interface {
    ~int | ~float64
    Compare(other interface{}) int
}

type Score int

func (s Score) Compare(other interface{}) int {
    o := other.(Score)
    if s < o {
        return -1
    } else if s > o {
        return 1
    }
    return 0
}

func Max[T Ordered](a, b T) T {
    if a.Compare(b) > 0 {
        return a
    }
    return b
}

func main() {
    s1 := Score(85)
    s2 := Score(92)
    
    fmt.Println(Max(s1, s2))  // 92
}

4. 型集合の交差と和集合

和集合(|)

package main

import "fmt"

// 整数型または浮動小数点型
type Number interface {
    int | int32 | int64 | float32 | float64
}

// または、基底型を使用
type Number2 interface {
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}

func Sum[T Number](values []T) T {
    var sum T
    for _, v := range values {
        sum += v
    }
    return sum
}

func main() {
    ints := []int{1, 2, 3, 4, 5}
    fmt.Println(Sum(ints))  // 15
    
    floats := []float64{1.1, 2.2, 3.3}
    fmt.Println(Sum(floats))  // 6.6
}

交差(複数の制約)

package main

import "fmt"

type Stringer interface {
    String() string
}

// 基底型がintで、かつStringerインターフェースを実装
type PrintableInt interface {
    ~int
    Stringer
}

type MyInt int

func (m MyInt) String() string {
    return fmt.Sprintf("値: %d", m)
}

func Display[T PrintableInt](x T) {
    fmt.Println(x.String())
}

func main() {
    var m MyInt = 42
    Display(m)  // 値: 42
}

5. 空の型集合

矛盾する制約は空の型集合を作ります。

package main

// ❌ エラー: intかつstringである型は存在しない
// type Impossible interface {
//     int
//     string
// }

// ❌ エラー: ~intとMyIntは重複している
// type MyInt int
// type Overlapping interface {
//     ~int
//     MyInt
// }

6. 標準ライブラリの例

constraints.Ordered

package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Min(10, 20))      // 10
    fmt.Println(Min(3.14, 2.71))  // 2.71
    fmt.Println(Min("apple", "banana"))  // apple
}

comparableの使用

package main

import "fmt"

// comparableは比較可能な型を表す
func Contains[T comparable](slice []T, value T) bool {
    for _, v := range slice {
        if v == value {
            return true
        }
    }
    return false
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println(Contains(numbers, 3))  // true
    
    words := []string{"apple", "banana", "cherry"}
    fmt.Println(Contains(words, "banana"))  // true
}

7. 使用制限

基本的でないインターフェースは変数にできない

package main

type Number interface {
    int | float64
}

func main() {
    // ❌ エラー: 基本的でないインターフェースは変数にできない
    // var x Number
    
    // ✅ OK: ジェネリクスの型制約としてのみ使える
    process := func[T Number](x T) T {
        return x * 2
    }
    
    _ = process
}

構造体のフィールドにできない

package main

type Number interface {
    int | float64
}

// ❌ エラー
// type Container struct {
//     value Number  // 基本的でないインターフェースは使えない
// }

// ✅ OK: ジェネリック構造体
type Container[T Number] struct {
    value T
}

8. 実用例

例1: 汎用的なスタック

package main

import "fmt"

type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

func main() {
    // intのスタック
    intStack := Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    intStack.Push(3)
    
    val, _ := intStack.Pop()
    fmt.Println(val)  // 3
    
    // stringのスタック
    strStack := Stack[string]{}
    strStack.Push("Hello")
    strStack.Push("World")
    
    str, _ := strStack.Pop()
    fmt.Println(str)  // World
}

例2: 数値演算

package main

import "fmt"

type Number interface {
    ~int | ~int64 | ~float32 | ~float64
}

func Sum[T Number](values ...T) T {
    var sum T
    for _, v := range values {
        sum += v
    }
    return sum
}

func Average[T Number](values ...T) float64 {
    if len(values) == 0 {
        return 0
    }
    sum := Sum(values...)
    return float64(sum) / float64(len(values))
}

func main() {
    fmt.Println(Sum(1, 2, 3, 4, 5))  // 15
    fmt.Println(Average(1.0, 2.0, 3.0, 4.0, 5.0))  // 3.0
}

例3: マップのキー制約

package main

import "fmt"

// comparableはマップのキーとして使える型
func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

func main() {
    m := map[string]int{
        "apple":  1,
        "banana": 2,
        "cherry": 3,
    }
    
    fmt.Println(Keys(m))  // [apple banana cherry]
}

まとめ: 一般的なインターフェースで覚えておくべきこと

型制約の種類

  1. 特定の型: int, string
  2. 基底型: ~int, ~string
  3. 和集合: int | float64
  4. メソッド付き: ~int + String() string

基本的な使い方

// 特定の型
type IntOnly interface {
    int
}

// 基底型
type Integer interface {
    ~int
}

// 和集合
type Number interface {
    int | float64
}

// 組み合わせ
type StringableInt interface {
    ~int
    String() string
}

使用制限

// ❌ 変数として使えない
// var x Number

// ❌ 構造体のフィールドとして使えない
// type Container struct {
//     value Number
// }

// ✅ 型制約として使える
func Process[T Number](x T) T {
    return x
}

// ✅ ジェネリック構造体で使える
type Container[T Number] struct {
    value T
}

実用的なアドバイス

package main

import "fmt"

// 1. シンプルな制約を定義
type Number interface {
    ~int | ~float64
}

// 2. ジェネリック関数で使用
func Max[T Number](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// 3. ジェネリック型で使用
type Stack[T any] struct {
    items []T
}

func main() {
    // 数値の比較
    fmt.Println(Max(10, 20))
    fmt.Println(Max(3.14, 2.71))
    
    // 汎用スタック
    stack := Stack[string]{}
    _ = stack
}

一般的なインターフェースは、型安全で再利用可能なコードを書くための強力な機能です。ジェネリクスと組み合わせることで、柔軟性と安全性を両立できます!

おわりに 

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

よっしー
よっしー

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

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

コメント

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