はじめに
まず Go言語特有の用語を説明します。
- slice
Go では可変長リストをスライスと呼びます。固定長配列と現在のサイズをもった構造体に、容量を超えたらリサイズする append 関数を用意したものです。 - map
hashmap のことです。python の dict と同じものになります。 - struct
Go では構造体 = クラスです。 - interface
構造体がもつべきメソッドを定義します。interface を受け取る関数には interface を満たすどんな構造体でも放り込めます。
Goのいいところ
型の宣言、初期化が覚えやすい
Go では配列の宣言はvar a []int のように[]を前につけます。前に[]をつけることで、配列の宣言と配列アクセスが区別されます。また型は func f(i int) のように、変数の後ろに書きます。
例えば「長さ2の固定配列の可変長配列」を使いたいとき Go だと
var a [][2]int
func f(a [][2]int)
変数の初期化は
var a = []int{1,2,3}
関数にその場宣言で配列を渡すとき
f([]int{1,2,3})
構造体の初期化は以下のような感じ
var d = Data{"name",1}
var d = Data{Name:"name",Value:1}
map は
var m = map[string]int{"key1":1,"key2":2}
変数の初期化は型{初期化}という形式になっており、一貫性があります。
関数もこの延長として捉えることができ
func f(a int) int {...}
前半が関数の型で、後半が関数の初期化と捉えらます。
関数は変数としても扱えます。
var p = func f(a int) int {...}
これも普通の変数と同じ形式で理解でき、func f(a int) int が変数の型で {...} が初期化です。
以下のように、その場宣言を使って、関数を引数とする関数に渡せます。
sort(lst, func(a, b int) int {return a-b})
関数も変数も初期化が Type{...} という形式で統一されていて素晴らしい。
型推定
Go 言語では変数を最初に初期化するとき := を使うと右辺値から型を推定してくれます。どの型にするかはルールが決まっていて、以下の場合は int になります。
a := 1
変数は何も指定しなければゼロ値で初期化される
int, float, string などの原子型のゼロ値は 0 や "" でスライスやマップは nil , 構造体は各フィールドがそのゼロ値になります。
オブジェクト指向言語のように「コンストラクタが何かやってる可能性」 を考えなくてよい。
「何も指定しなければゼロ値で初期化」 これで考えること、覚えることが少なくなります。
nil 耐性のあるデータ構造
スライスは nil で初期化されますが、スライスに要素を追加するappendメソッドは
list = append(list,a)
という使い方で、list が nil の場合新しくスライスを確保してくれます。
疎結合なインターフェイス
interface はそれを満たすオブジェクトが持つメソッドの型を定義します。例えば以下のようにStringerインターフェイスを定義します。
type Stringer interface {
String() string
}
String() string というメソッドを持つ型はこの Stringer インターフェイスを満たすことになります。
type A class {
Name string
}
// Implements Stringer interface
func (a *A) String() {
fmt.Printf("Name : %s",a.Name)
}
ここでは Stringer インターフェイスを満たす構造体を定義しているのですが、他の言語のように implements Stringer のような文は必要がありません。
例えば Writer インターフェイスは io パッケージに定義されているのですが、io を import せずに Writer インターフェイスを満たす型を作れます。
nil でも呼び出せるメソッド
クラスAのメソッドの定義の仕方は
func (a *A) method () {}
と書きます。a *A はレシーバーとよばれ、呼び出し元のオブジェクトが入ります。実はこの a が nil でも呼び出しが成功します。Go言語でメソッドとは、第一引数が構造体またはそのポインタである関数、を簡単にかけるようにしたものなので nil を入れても動くものが作れます。
ポインタと実体を透過的に取り扱える
C言語だと
a.f()
a->f()
を使い分けないといけないのですが、Go では構造体でもポインタでもa.f()でメソッドやフィールドを呼び出せます。考えてみればポインタであれ実体であれ、それから呼び出すものは一緒です。コンパイラは呼び出し元の型が分かっていれば問題ありません。
諦めがよい
Goでは言語の開発者側の都合が優先されているような仕様がいくつかあります。
- 関数のオーバーロードが無い
コンパイラを単純にするため - 標準の引数処理のパッケージが貧弱
オプション引数は位置引数の前に置かなければならない。ロング、ショートの区別がない。理由はコードを単純にするため - ケツカンマ必須
カンマをつなげて複数行に分けて記述するときは必ず最後にカンマがいる。これもコンパイラを単純にするため
defer
golang では python の with と同じ目的の文として、defer があります。これは defer の後に書いた文の実行を遅延させることができる機能で、その文を含んでいる関数を抜けるときに、遅延させた関数が実行されます。
f,err := os.Open(file)
if err != nil {
log.Fatal(err)
}
defer f.Close()
f.WriteString("abc")
このように書きます。deferのいいところは ifやforの中で書いても、そのスコープを抜けても遅延が続くことです。なので以下のようなことができます。
var out *os.File = os.Stdout
if file != "" {
f,err := os.Open(file)
if err != nil {
log.Fatal(err)
}
defer f.Close()
f = out
}
out.WriteString("abc")
file が空文字のときは標準出力に、そうでないとき、file を開いて書き込むという処理なのですが、defer は if の中に入ったときのみに「発動」しますが、Close の実行は一番最後になります。
つまり、リソースを確保したときだけ、最後に解放操作を行う、ということができます。
クロージャ + コールバック関数
c言語ではvoidポインタでデータ型を扱える汎用データ構造など、いろいろできるのですが、コールバック関数の扱いがきついことがあります。例えば木構造で各ノードを訪れて関数を実行するというとき
void walk(Node node, void (*dosome)(void*)){
Node n;
dosome(node->content);
for(n=node->childs;n;n=n->next){
walk(n,dosome);
}
}
こんな感じになるのですが、このとき dosome が例えば print 系の関数だったらいいのですが、dosome に tree の各ノードの情報を持って帰ってくることはできません。その場合dosomeの関数の形を変える必要があり、そうするとwalkの一般性がなくなってしまいます。
しかし golang ではクロージャを使ってこれを解決できます。golang では全ての関数はクロージャであり、自身が定義されたときに、自身を含むスコープ内に定義されている変数にアクセスできます。
Go で関数Walkを定義します。ここではまだクロージャは使っていません。
func (root *Node) Walk(do func(*Node)) {
do(root)
for _,c := range root.Childs {
c.Walk(do)
}
}
これをこんなふうに使えます。これがクロージャです。
func Path(root *Node) []*Node {
var path []*Node
root.Walk(func(n *Node) {
path = append(path,n)
})
return path
}
Walk にいれた関数が、その定義の外の変数 path にアクセスできています。この場合 Walk に渡している関数にとってpathという変数が、グローバル変数のように扱えます。
このクロージャ、実はあまり害はありません。「すべての関数はクロージャ」としても、まず一番外の関数と変数はふつうの関数、グローバル変数になります。スコープの内側の関数と変数ですが、その内部で定義されてない変数を使うと普段はエラーになってたのですから、もともと意味のないところに意味をつけただけであり、いままでのフィーリングを壊すことはありません。
if 文のはじめに変数が定義できる
if length := len(a[i]); length > lenmax {
lenmax = length
}
こういう書き方ができます。また以下のようにエラー処理を短くかけます。
if err := f() err != nil {
/*process err*/
}
Go の注意点
UpeerCamelCase
golang では大文字から始まるフィールド名が外のパッケージから見える変数になります。たまに自分で作った構造体を標準ライブラリの関数に渡すというシチュエーションがあり、このときフィールド名が大文字から始まってないと Unexported field のようなエラーになります。なので大文字から始めるクセをつけたほうがよいです。
type A struct {
FieldA,FieldB int
}
タイプ名を大文字でフィールド名を小文字という使いわけをしてきた人は、どちらも大文字にして
type A struct {
Name Name
}
として大丈夫です。
名前つき返り値
golang では返り値に名前をつけて
func f(a type1, b type2) (c type3, d type4) {/*do something*/}
のようにかけるのですが、このとき返り値を変更して、後ろに何もつけない return 文で返り値を返却することができます。
func f(a type1, b type2) (c type3, d type4) {
c = 1
d = 2
return
}
ケツカンマ必須
Go言語では関数の引数の数が多いときなど dosome(a,b,c)を以下のように書けるのですが
dosome(
a,
b,
c,
)
このとき最後のカンマは必須となります。理由はコンパイラを単純にするためです。
:=と複数変数とスコープの組み合わせにより生じる問題
golang では := を使って型推定を使って新しい変数を定義できます。複数変数の場合、左辺に新しい変数が一つでも含まれていればこの書き方が可能で、定義済みの変数に対しては再代入になります。
a := 1
a,b := CreateAB() // a は代入, b は初期化
しかしスコープの外で定義されている変数の場合、再代入ではなくスコープ変数の初期化になってしまいます。
var a int
if a,err := GetA(); if err != nil {
log.Fataf("%d,%e",a,err)
}
fmt.Println(a) // 常に0
また:=は構造体変数の代入には使えません。
var args struct {
dir string
}
if args.dir,err := os.Getwd(); err != nil { //compile error
log.Fatal(err)
}
と書きたいところですが、実際は以下のような書き方になります。
var args struct {
dir string
}
var err error
if args.dir,err = os.Getwd(); err != nil { // = は使える
log.Fatal(err)
}
var args struct {
dir string
}
dir,err := os.Getwd()
if err != nil {
log.Fatal()
}
args.dir = dir
メソッドのレシーバが構造体かポインタかの違い
golang ではメソッドは以下のように関数の前にレシーバ引数というものを書くことで定義します。
func (a *A) method(b int) int {}
このときレシーバとして構造体のポインタが普通ですが、実体をとることもできます。
func (a A) method(b int) int {}
基本的にメソッドのレシーバはポインタにします。またポインタにすると、構造体の中身を変更できます。ただし構造体を返す関数get()にチェーンして get().method() のようにすると、レシーバがポインタのときエラーになります。これは get() の返却値は右辺値専用で、アドレス指定不可な一時変数だからです。この書き方をしたいなら、get() の戻り値をポインタにするか、レシーバを構造体にする必要があります。
Goの悪いところ
解説が少ない
Go Tour で基本文法は学べるんですが、誰かが作ってくれたガイドは、他の言語より充実してません。おそらくみんな Doc を読んでます。で Doc を読んでプログラムするのが良い慣習ではあるので、誰も「こうしたいときはこう書けばいい」みたいな web ページを作らないのかもしれません。
Doc が難しい
Doc には、関数などの正確で簡潔で完結な文章が書いてあるのですが、読んでいても使える気になりません。ただ、ものは試しで使ってみると案外普通に使えます。
「始めかた」が難しい
go 言語にはすごい便利な機能がたくさん入ってるのですが、部品の説明はあるけど組み合わせかたがないという感じで困ります。例えば複数パッケージからなるコードを作る時、チュートリアルはソースコード一つのもので説明されるので、init 関数や go generate との親和性、フィールド名を大文字から始めることなど、いろいろなことが後あとわかりました。
c言語との連携が微妙
cgo という仕組みがあり、ぱっとみ、連携は十分にできそうなんですが、メモリの番地のやりとりはやめたほうがいいです。GC がメモリを追跡しているのは Go の世界の中だけなので、c言語の世界にわたすと見失って解放してしまうなどの問題があります。
Map が順序を保存しない
ソートするなら一応ワンライナーで対処できますが、入れた順にするには別に配列が必要です。go言語を「楽な静的言語」として使いたいので、デフォルトで順序を保存してほしかったです。
var m map[int]string
for _, k := range slices.Sorted(maps.Keys(m)) {
fmt.Println("Key:", k, "Value:", m[k])
}
結論
簡潔で覚えやすく、最初に習うべきプログラム言語はこれと言っていいと思います。python はもっと簡単なのですが、python から始めると型が隠蔽されている弊害がでそうなので、Go言語が最もよいと思います。