Top▲
マーリンアームズ サポート   翻訳   コンサル   講座   アプリ   コラム

初めてのGo言語 —— 他言語プログラマーのためのイディオマティックGo実践ガイド    Jon Bodner著    武舎広幸訳

付録A Go言語のまとめ

武舎 広幸

付録A付録Bは日本語版オリジナルの内容です。付録Aでは、この本の内容に沿った形で、文法事項などをできるだけ簡潔に、ほかの言語の経験がある程度ある人なら、意味がわかるようにまとめました。参照用にご利用ください。

付録Bには訳者が作成した、長くはないけれど、何らかの機能を果たすプログラムを集めました。原著の最初のほうの章には、まとまったコードがあまり書かれていないので、付録Bの例題をざっと読んでおくと、本文の内容がわかりやすくなると思います。とくに最初から「B.4 コマンド行計算機——コマンド行引数、文字列の置換と正規表現、外部コマンド」までは比較的単純な例になっているので、Goに馴染みのない方は最初に読んでおくことをおすすめします。

一部の内容(A.14.1からA.14.3)は、原著の内容を強く反映しているためウェブでは非公開とさせていただきます。あしからずご了承ください。

A.1 ほかの言語で(あまり)見られない点

import (
    "fmt"
    _ "time"
)

A.2 型とゼロ値

Go言語では以下に示す型(基本型と合成型、その他)が用いられます。各型には「ゼロ値(zero value)」があり、初期値を指定しないで変数を宣言するとゼロ値が初期値となります(以下の説明において、章の記載がないものは2章で説明しています)。

なお、スライス、マップ、関数、インタフェース、チャネルはポインタとして実装されています。ポインタとして実装されている型のゼロ値はすべてnilとなります(nilは値がないことを示す型のない識別子)。

A.2.1 基本型(組み込み型)

ファイルから読み込んだデータは多くの場合バイト列(Go的に書くと「byteのスライス」つまり[]byte)として読み込まれます。たとえば次のコードで、変数bの型は[]byteとなります。

  b, err := os.ReadFile("testdata/xxx.json")

byteuint8の別名なので[]uint8でも同じですが、整数として扱っているわけではないので[]byteとしたほうが意図が明確になります。ただし、「fmt.Printf("%T", b)」としてbの型を出力すると「[]uint8」と表示されてしまうので、慣れるまでは要注意です。

文字列に関する操作をするのに、[]byteを直接操作できる場合もありますし、文字列(string)に変換してから行う必要がある場合があります。概して、文字列に変換してから操作したほうがわかりやすいでしょうが、バイト列を直接操作したほうが効率的でしょう。

A.2.2 合成型(複合データ型、コンテナ型)、その他

A.3 変数と定数の宣言

変数は「変数名」「型」の順番で宣言するのが基本ですが、初期値の指定などのためにさまざまな形式が用意されています。

A.3.1 基本型の変数の宣言

下の例はint(整数)の例。ほかの型についても同じように宣言できます。

var x int = 10  // 型と初期値を指定する
var x = 10  // 型を省略する(デフォルトの型になる。この場合int)
var x int  // 初期値を省略する(intの「ゼロ値」が入る。この場合0)
var x, y int = 10, 20  // 多重代入
var x, y int  // 初期値の省略(ゼロ値が入る)
var x, y = 10 "hello"  // 型の異なる変数の宣言
x := 10  // 「var x = 10」と同じ。関数内でのみ使える。一番簡単。

// 宣言リスト
var (
    x    int
    y        = 20
    d, e     = 40, "hello"  // eは文字列
)

A.3.2 配列とスライスの宣言

// 配列(イミュータブル、つまり変更できない。サイズも固定)。配列やスライスの大きさは「len(x)」でわかる
var x [3]int // サイズを指定する
var x = [3]int{10, 20, 30}
var x = [12]int{1, 5: 4, 6, 10: 100, 15}
var x = [...]int{10, 20, 30}  // サイズは要素数で決まる
var x [2][3]int  // 多次元配列をシミュレート

// スライス(サイズ可変の配列)
var x = []int{10, 20, 30}  // サイズを指定すると配列になってしまう
var x = []int{1, 5: 4, 6, 10: 100, 15}
var x = []int{}  // nilスライス
var x [][]int  // 多次元のスライスのシミュレート

A.3.3 スライスへの要素の追加

var y []int  // []
y = append(y, 10)  // [10]

var x = []int{1, 2, 3}  // [1 2 3]
x = append(x, 4)  // [1 2 3 4]
x = append(x, 5, 6, 7)  // [1 2 3 4 5 6 7]

x = []int{1, 2}  // [1 2]
z := []int{20, 30, 40}  // [20 30 40]
x = append(x, z...)  // [1 2 20 30 40]  // 別のスライスの全要素を追加

A.3.4 サブスライス(スライスの要素の削除)

スライスの一部の要素を削除したものを作りたい場合は、「:」を使って「サブスライス」を作ります(「3.2.6 スライスのスライス」)。サブスライスを作っても、領域は共有されているので、要素を変更すると共有しているすべての(サブ)スライスが影響を受けることに注意が必要です。

x := []int{1, 2, 3, 4}  // [1 2 3 4]
y := x[:2]    // [1 2]
z := x[1:]    // [2 3 4]
d := x[1:3]   // [2 3]
e := x[:]     // [1 2 3 4]

A.3.5 マップの宣言とリテラル

詳しくは「3.4 マップ」を参照してください(XXX/exXXX.go)。

    intMap := map[string]int{} // 文字列→整数のマップで要素がないもの
    intMap["abc"] = 1
    intMap["あいう"] = 10
    fmt.Println(intMap["abc"]) // 1
    fmt.Println(intMap["あいう"]) // 10

    m := map[string]any{  // interface{}も可 (go 1.18でanyが導入された)
        // any にはどのような型の値も入れられる
        "文字": "あいう",
        "数字": 123,
        "マップ": map[string]int {
            "加奈": 32,
            "涼子": 23,
        },
    }
    fmt.Println(m["文字"]) // あいう
    fmt.Println(m["数字"]) // 123
    fmt.Println(m["マップ"]) // map[加奈:32 涼子:23]
    x := m["マップ"].(map[string]int) // 「7.10.1  型アサーション」
    // m["マップ"]のままではanyなので、そのまま m["マップ"]["加奈"]とはできない
    // 型アサーションで、map[string]int だと(強制的に)仮定する
    fmt.Println(x["加奈"]) // 32
    fmt.Printf("%T\n", x) // map[string]int // %Tで型を出力

A.3.6 makeを使ったスライスやマップの生成

x := make([]int, 5)  // サイズ5のスライス
x := make([]int, 5, 10)  // サイズ5、キャパシティ(確保された容量)10のスライス

ages := make(map[string]int, 10)  // サイズは0になるが、要素10個分のメモリを確保。
// 文字列→整数 のマップ

A.3.7 定数の宣言

詳しくは「2.3 定数」を参照してください。

const x int64 = 10  // 型付きの定数宣言
const y int64 = x
// var z int = x  // 型が違うのでコンパイル時のエラーになる

const a = 2 * 4  // 型なし(untyped)の定数宣言
var b int = a  // OK。型なしの定数は代入できる(stringなどには代入できない)
var c float64 = a  // OK。同様
var d byte = a  // OK。同様

const (  // 宣言リスト(複数の定数をまとめて宣言)
  idKey   = "id"
  nameKey = "name"
)

A.4 構造体の宣言とリテラル

構造体(「3.5 構造体」)を使うことで、複数のデータを併せもつ独自の型(type)を定義できます。Goにクラスはありませんが、構造体とインタフェースを併用することで、類似の(より優れた?)機能を実現できます(後述の「A.10 メソッド」も参照)。

type person struct {  // person型の宣言
    name string  // 「フィールド名」はnameで、その型はstring
    age  int
    pet  string
}

var fred person  // person型の変数fredを宣言
bob := person{}  // 空の構造体リテラルを使う。全フィールドがゼロ値
kanako := person{  // 構造体リテラルを使った変数宣言
    "加奈子",  // name  // 全フィールドの値を順番に指定する
    40,        // age
    "猫",      // pet   // 最後に「,」がある点に注意
}
hiroe := person{  // 構造体リテラル。「フィールド名」と「値」を指定
    age:  30,     // 指定されていないもの(pet)はゼロ値("")になる
    name: "広江",
}

// 無名構造体
var person struct {  // 変数personの型宣言
    name string
    age  int
    pet  string
}
person.name = "ボブ"
person.age = 50
person.pet = "dog"

pet := struct {  // 型を指定して
    name string
    kind string
}{               // 初期値を指定
    name: "ポチ",
    kind: "dog",
}

A.5 型のエイリアス

キーワードtypeを使って、型のエイリアスを作れます。

Fooが定義されているとき、次のようにすることで、FooBarは同じフィールドとメソッドをもつようになります。

type Bar = Foo

A.6 iota

Goに列挙型はありませんがiota(「7.2.7 iotaと列挙型」)を使うと、1ずつ増加する値を一連の定数に割り当てることができます。

type ServerStatus int  // サーバの状況

const (
    Unknown ServerStatus = iota  // 不明  値は0になるが、0を使うのは非イディオム的
    Up  // 動作中  値は1
    Down  // ダウン   値は2
)

A.7 制御構造

if文、switch文、for文を使います。ループにwhile文などはありません。goto文もあります。詳細は4章を参照してください。

A.7.1 ifforの基本形

for count:=1; i<10 ; count++ {  //「条件」を指定しないと無限ループになる
  if num, err := readUserAnswer(count); err != nil || num < 1 || 10 < num  {
    fmt.Println("ハズレ!")
  } else if num != answer {  // 必ず 「} else if」を同じ行に書く
    fmt.Println("残念でした。ハズレです。")
  } else {
    printSuccessMessage(count)
    break  // forループを抜ける
  }
}

A.7.2 for-rangeの例

dict := map[string][]string{ // string -> []string(文字列のスライス)のマップ
  "fungible": []string{"代替可能な", "代替性のある", "代替物"},
  ...,
}
eng = "fungible"
translations := dict[eng]  // dictはstring->[]stringのマップ
for _, t := range translations { // インデックス(添字)は無視
  fmt.Printf("%s|%s\n", eng, t);
}

A.7.3 switch

loop:  // 下のbreakでループを抜けるための「ラベル」
  for count := 1; ; count++ {  // 無限ループ
    switch num, err := readUserAnswer(count); {
    case err != nil || num < 1 || 10 < num:
      fmt.Println("おかしな入力!")  // breakがなくても、このcaseはここで終了
    case num != answer:__
      fmt.Println("ハズレ!")  // breakがなくても、このcaseはここで終了
    default:
      printSuccessMessage(count) // 当たったときのメッセージを表示
      break loop // forループを抜ける。breakだけだとdefaultを抜けるだけになってしまう
  } // forの終わり

A.8 演算子

基本的にはC言語などとよく似ています(2章参照)。ただし、変数iをインクリメント(+1)するのはi++で、++iはエラーになります。同様に--iも使えません。

チャネルに関連して<-が用いられます(10章参照)。

ch <- x
チャネルchxを送信
x = <- ch
チャネルchから受信した値をxに代入

A.9 関数

比較的単純な関数の定義のパターンを示します(「B.3.2 関数とループ」に例があります。詳しくは5章参照)。

import "errors"
...
func 関数名(〈引数とその型〉) (〈戻り値の型〉, error) {
    if 〈エラーの条件〉 {
        return 〈戻り値の型のゼロ値〉, errors.New("エラーメッセージ")
    }
    ...
    return 〈戻り値〉, nil
}

A.10 メソッド

Goに「クラス」やその「継承」の機能はありませんが、型(type)に対してメソッド(型メソッド)を定義できます(「7.2 メソッド」)。「型」とその型用の「型メソッド」は同じモジュール(通常は同じファイル)に書きます。

次の例は構造体に付随するメソッドを定義するものです(ch07/ex0701.go)。

// 型Personを定義
type Person struct {
    LastName string  // 姓
    FirstName string // 名
    Age int  // 年齢
}
// 型Personに付随するメソッドStringを定義(PersonにメソッドStringを付加)
func (p Person) String() string { // 「(p Person)」がレシーバの指定
    return fmt.Sprintf("%s %s:年齢%d歳", p.LastName, p.FirstName, p.Age)
}

組み込みの型(intなど)には直接メソッドは定義できませんが、基本型から別の型を作って(type MyInt int)、その型に付随するメソッドを定義すれば、intに対して定義したかのように使えます。

A.11 継承

上に書いたようにGoに「クラス」はありません(したがって、クラス間の継承機能もありません)が、構造体とそれに付随するメソッドを定義することで、クラスのような機能を実現できます。

さらに、「埋め込みフィールド」を利用することで、構造体に上下関係を入れ、継承と類似の機能を実現できます(「7.3 埋め込みによる合成」)。

A.12 インタフェース

インタフェースはGoで唯一の抽象型(実装を提供しない型)で、次の2つの側面をもちます(「7.5 インタフェースとは」)。

  1. メソッドの集合——他言語の「インタフェース」のように、特定のクラスが満たすべき要件(実装するべき一群のメソッド)を示す
  2. ——変数がインタフェースを基盤とする型をもつことで、さまざまなことができる。たとえば、任意の型の値を代入できる変数を定義できる

2.の例として特によく使われるのがinterface{}(Go 1.18からはanyとも書けるようになりました)。これは、「0個のメソッドが定義された型」ということになるので、任意の値がこの条件を満たすことになります。したがってanyと宣言された変数には、任意の型の値を記憶できます。

A.13 ゴルーチン

並行処理をするのにゴルーチン(10章)が使えます(「B.6 ゴルーチン、チャネル、WaitGroup」にサンプルあり)。

  1. chan string
  2. chan <-string
  3. <- chan string
ch := make(chan int, 10)  // バッファのキャパシティ10のチャネルの生成

A.13.1 チャネルからの読み込みのパターン

チャネルからの読み込み時には次のようなパターンが使われます。

A.13.1.1 変数への代入

a := <-ch  // チャネル変数chから値を読み込み、aに代入する

A.13.1.2 無視する

    <-ch  // チャネルchから読み込み、読み込んだデータは無視。ひとつ「から読み」される

A.13.1.3 select文のcaseでの代入

select {  // 複数のチャネルのいずれかから読み込む
case v := <-ch1:  // 読み込めるチャネルが複数ある場合、どのチャネルから読み込まれるかは不定
    fmt.Println("ch1:", v)
case v := <-ch2:
    fmt.Println("ch2:", v)
...
}

A.13.1.4 for-rangeループ

for v := range ch {  // チャネルchがクローズされるまで、値を読み込む
    fmt.Println(v)   // 読むものがなければ待たされる(ポーズする)
}

A.14 Goの「イディオム」

本文にあげられているGoの「イディオム」や「慣習」などをまとめました。ほかにも、(逆に)「イディオム的でないもの」や、コード中でよく使われるパターンもあげました。登場順に並んでいますが、Go言語で特に重要、あるいは特徴的と思われるものは太字にしました。

A.14.1からA.14.3は、原著の内容を強く反映しているためウェブでは非公開とさせていただきます。あしからずご了承ください。

A.14.1 イディオム的なもの

A.14.2 「イディオム的」でない例

A.14.3 その他、Goの「哲学」が感じられる選択

A.14.4 備忘録

その他、覚えておくと役に立つ場面がありそうな事柄や、初見では意味が即座に理解できないと思われるデータ型などをあげておきます。

以下に初心者には意味の把握が簡単ではないと思われる表現をあげておきます。

    m := map[string]any{}