Go言語入門者である私が気づいたことを長々と書いています。
既に他の方が言及されていることも多いです。また初心者でよくわかっていないことも多いためお手柔らかにお願いします。
なお、順番は適当です。
Go Advent Calendar 2018 24日目の記事として投稿させていただいております。
(元々の方が投稿されていなかったようなので、代わりに入れさせて頂きました。)
継承の代わりとして匿名フィールドを用いた場合、型の判定がうまくいかない
Go言語はオブジェクト指向言語ではありませんが、構造体やレシーバを用いることでオブジェクトのメンバを「呼び出す」ことができます。
まず、Animal
「クラス」を作ってみましょう。そして自己紹介するためのレシーバDescribe()
も定義します。
type Animal struct {
Age int
}
func (animal *Animal) Describe() {
fmt.Printf("I am %v years old.\n", animal.Age)
}
こんどはPerson「クラス」を追加しましょう。人間は動物なのでPerson「クラス」ではAnimal「クラス」を継承したいですね。Go言語では匿名フィールドという機能を使えば、Person構造体にAnimal構造体の機能も持たせることができます。
type Person struct {
Animal
Name string
}
func (person *Person) Describe() {
fmt.Printf("I am %v, %v years old.\n", person.Name, person.Age)
}
ついでにPlant
(植物)も定義します。植物には口がないため自己紹介はできません。
type Plant struct {}
ここまでできたら、試しにAnimalとPerson, Plantのオブジェクトをそれぞれ生成して自己紹介させてみましょう。
func callDescribe(obj interface{}) {
switch obj.(type) {
case *Animal:
(obj.(*Animal)).Describe()
default:
fmt.Println("It is not an animal.")
}
}
func main() {
var animal interface{} = &Animal{10}
var person interface{} = &Person{Animal{20}, "Joe"}
var plant interface{} = &Plant{}
callDescribe(animal)
callDescribe(person)
callDescribe(plant)
}
実行すると以下のようになります。
I am 10 years old.
It is not an animal.
It is not an animal.
動物であるはずの人間が「人間でない」と判定されてしまいました。
解決法
対象が動物かどうかを判定するときに、インターフェースを使えばうまくいきます。
type LooksLikeAnimal interface{
Describe()
}
func callDescribe(obj interface{}) {
switch obj.(type) {
case LooksLikeAnimal:
(obj.(LooksLikeAnimal)).Describe()
default:
fmt.Println("It is not an animal.")
}
}
実行結果
I am 10 years old.
I am Joe, 20 years old.
It is not an animal.
期待通りに表示されました。
レシーバを使う際に注意が必要な場合
以下のコードを見てください。
type Animal struct { }
func (animal *Animal) DescribeP() {
fmt.Println("I am a pointer of animal.")
}
func (animal Animal) Describe() {
fmt.Println("I am animal.")
}
func main() {
var animal Animal = Animal{}
var panimal *Animal = &Animal{}
animal.Describe()
animal.DescribeP()
panimal.Describe()
panimal.DescribeP()
}
実行結果は以下のようになります。
I am animal.
I am a pointer of animal.
I am animal.
I am a pointer of animal.
どうやらGo言語のレシーバはポインタ型でもそうでなくても同じ働き(値渡しと参照渡しという違いはありますが)ができるようです。
(ちなみに func (animal *Animal) Describe()
を追加で宣言するとmethod redeclared: Animal.Describe
エラーになります)
では、main関数を以下のように書き換えて試してみます。
func main() {
var panimal *Animal = nil
panimal.DescribeP()
panimal.Describe()
}
出力はこのようになります。
I am a pointer of animal.
panic: runtime error: invalid memory address or nil pointer dereference
1つ目のポインタレシーバの呼び出しはうまく行きましたが、2つ目の呼び出しは実行時エラーで失敗しました。
考えてみれば当たり前なのですが、ポインタでないレシーバを使う場合は気をつける必要があるようです。
また以下のような場合はどうでしょうか。
func main() {
var animal Animal
var i interface{} = animal
animal.DescribeP() // ok
i.(Animal).Describe() // ok
i.(Animal).DescribeP() // err: cannot take the address of i
}
今度はコンパイルが通らなくなってしまいました。これも技術的な制約であり十分理解できるのですが、やはりポインタレシーバを使う場合も気をつけなければいけないようです。
参考
https://skatsuta.github.io/2015/12/29/value-receiver-pointer-receiver/
Arrayの長さ省略表現
Go言語では配列の宣言及びコピーは以下のようにできます。
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var arr [6]int = primes
fmt.Println(arr)
}
しかしながら、最初のprimes
の宣言はすこし冗長です。例えばC言語の場合、配列の宣言と初期化を同時に行う場合は以下のように要素数を省略できます。
int primes[] = {2, 3, 5, 7, 11, 13};
Go言語で同じことをやるとどうなるでしょうか。
func main() {
primes := []int{2, 3, 5, 7, 11, 13}
var arr [6]int = primes
fmt.Println(arr)
}
cannot use primes (type []int) as type [6]int in assignment
エラーになってしまいました。
解決法
Go言語で配列宣言時の要素数を省略する場合は...
を用います。
func main() {
primes := [...]int{2, 3, 5, 7, 11, 13}
var arr [6]int = primes
fmt.Println(arr)
}
スライス
(2019.4.15 追記)
先程の例のprimes := []int{2, 3, 5, 7, 11, 13}
は配列ではなくスライスの初期化を意味します。
以下に示すように、Go言語では配列よりもスライスを活用すると便利です。
// 長さが0のスライス
var a = make([]int, 0)
a = append(a, 12)
// 最初から初期化されているスライス
var b = []int{1, 1, 2, 3, 5}
fmt.Println(a, b) // 出力: [12] [1 1 2 3 5]
エラーハンドリング
Go言語にはtry
...catch
のようなエラー処理機構がありません。
......本当はある(panic
)のですが、通常は使うことが推奨されません。(回復処理を期待できない場合のみ使う)
参考: https://dave.cheney.net/2012/01/18/why-go-gets-exceptions-right
In
panic
ing you never assume that your caller can solve the problem. Hence panic is only used in exceptional circumstances, ones where it is not possible for your code, or anyone integrating your code to continue.
よって、エラーは基本的に全て戻り値で返します。例:
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("test.txt")
if err != nil {
fmt.Fprintf(os.Stderr, "File create error\n")
}
defer file.Close()
}
では、さらにディレクトリを作成したくなった場合、main関数をどのように書き換えればよいでしょうか。
func main() {
file, err := os.Create("test.txt")
if err != nil {
fmt.Fprintf(os.Stderr, "File create error\n")
}
err := os.Mkdir("hoge", 0777)
if err != nil {
fmt.Fprintf(os.Stderr, "File create error\n")
}
defer file.Close()
}
これは一見正しそうですがコンパイルエラーです。
no new variables on left side of :=
Go言語で:=
は変数の宣言と代入を同時に行ってくれる演算子ですが、変数は二重に宣言することができないため、二回目にerr := ...
としたところでエラーになります。よって、二回目の:=
を=
に書き換えれば良いです。
func main() {
file, err := os.Create("test.txt")
if err != nil {
fmt.Fprintf(os.Stderr, "File create error\n")
}
err = os.Mkdir("hoge", 0777)
if err != nil {
fmt.Fprintf(os.Stderr, "File create error\n")
}
defer file.Close()
}
しかし、別の解決策もあります。実はこの例では、ファイルとディレクトリの作成順を入れ替えると:=
を使用したままでもエラーが発生しなくなります。
func main() {
err := os.Mkdir("hoge", 0777)
if err != nil {
fmt.Fprintf(os.Stderr, "File create error\n")
}
file, err := os.Create("test.txt")
if err != nil {
fmt.Fprintf(os.Stderr, "File create error\n")
}
defer file.Close()
}
Go言語の仕様上、複数の変数に対し:=
で代入するとき、2つ目以降の変数については既に存在していてもエラーにならないのです。(型が合わないときはエラーになります)
上の例ではエラー処理を2回行っているのですが、少し面倒ですね。これを1回で済ます方法はないでしょうか。
func main() {
err := os.Mkdir("hoge", 0777)
if err == nil {
file, err := os.Create("hoge/test.txt")
if err == nil {
file.WriteString("abc")
defer file.Close()
}
}
if err == nil {
fmt.Println("Success!")
} else {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
}
エラー処理を最後にまとめてみました。ついでにファイルへの書き込みも行っておきました。これは一見うまく機能するように見えます。
では意図的にエラーを発生させてみましょう。4行目をfile, err := os.Create("fuga/test.txt")
とすれば、書き込み先のディレクトリが存在しないためエラーになるはずです。
$ go run test.go
Success!
$ ls hoge
. ..
$ ls huga
ls: 'huga' にアクセスできません: そのようなファイルやディレクトリはありません
エラーになりませんでした。しかしながらもちろんファイルは作成されていません。なぜでしょうか? ぜひ考えてみてください。
番外編: Go言語でtry...catch
に近いことをやる
先程Go言語にはtry...catch
が無いと書きましたが、似た仕組みはあります。それがpanic
です。
では、panic
を使用してどエラーを起こすにはどのようにすればよいのでしょうか。
func errFunc(name string) {
panic(name + " does not want to do anything.")
}
func main() {
errFunc("Bob")
fmt.Println("Success")
}
$ go run test.go
panic: Bob does not want to do anything.
panic
によってプログラムが強制終了されました。ではこれをcatch
するためにmain関数を書き換えます。
func main() {
defer func() {
fmt.Printf("recovered from panic: ")
fmt.Println(recover())
}()
errFunc("Bob")
fmt.Println("Success")
}
$ go run test.go
recovered from panic: Bob does not want to do anything.
うまくcatch
できています。ですが、"Success"
が表示されません。それは、main関数全体がいわばtry...catch
のtry
節のようになっているからです。では、finally
を実現してみましょう。
func main() {
func(){
defer func() {
fmt.Printf("recovered from panic: ")
fmt.Println(recover())
}()
errFunc("Bob")
}()
fmt.Println("Success")
}
$ go run test.go
recovered from panic: Bob does not want to do anything.
Success
一応うまくできました。しかし見た目が気持ち悪いですね。実際に使う場合はtry
節の内部のみを別の関数として宣言した方がよさそうです。
for range
Go言語にも、foreach
のような構文が用意されています。それがrange
です。
func main() {
for p := range [...]int{2, 3, 5, 7, 11} {
fmt.Println(p)
}
}
実行結果:
$ go run test.go
0
1
2
3
4
期待した結果にはなりませんでした。正しくは以下のようにします。
func main() {
for _, p := range [...]int{2, 3, 5, 7, 11} {
fmt.Println(p)
}
}
実行結果:
$ go run test.go
2
3
5
7
11
小さな違いが大きなバグを生む典型例です。
セミコロン自動挿入による文法制約
Go言語で複雑な計算をしたいとします。あまりにも複雑なため1行で収まらず、下のように途中で改行を入れました。
func main() {
a := 2 + 3 + 5 + 7 + 11
+ 13 + 17 + 19
fmt.Println(a)
}
$ go run test.go
# command-line-arguments
./test.go:7:16: +13 + 17 + 19 evaluated but not used
コンパイルが通りませんでした。しかし、以下のように書き換えるとうまくコンパイルできます。
func main() {
a := 2 + 3 + 5 + 7 + 11 +
13 + 17 + 19
fmt.Println(a)
}
なぜこのようになるかというと、Go言語では各行末に自動的に文の終わりを示す;
を挿入しているからです。2つ目の例がうまくいったのは、行の末尾が記号で終わる場合は;
を挿入しない、という簡単なルールによって制御されているからです。どこかのes6とは大違いですね。(実際はそこまで単純ではないようですが)
すなわち、単純な制御構文の例でも同じことが起こります。
func main() {
a := 2
if a != 1 // コンパイルエラー
{
fmt.Println("a is not 1")
}
}
Tclみたい
名前空間とパッケージ名
Go言語でプログラムを書くとき、最初にpackage main
と記述します。これは、「このファイルはmain
パッケージに属している」という意味ですが、main
以外のパッケージを作って名前空間を分けたい場合はどのようにすればよいのでしょうか。Go言語ではこのパッケージ名はディレクトリ階層に対応しています。
$ tree
.
├── main.go
└── pub
└── sub
└── file.go
2 directories, 2 files
$ cat pub/sub/file.go
package sub
import "fmt"
func Sub() {
fmt.Println("Hello from sub")
}
$ cat main.go
package main
import "./pub/sub"
func main() {
sub.Sub()
}
上の例を見てください。パッケージ名はファイル名ではなくディレクトリ名と対応していることがわかります。
interface{}
が nilにならない
Go言語においてあらゆる値を代入できる方としてinterface{}
があります。
そんな便利なinterface{}
ですが、一度nil
を代入すると大変な厄介者に...。
type A struct {}
func f() *A {
return nil
}
func main() {
var i interface{} = nil
var j interface{} = f()
if i == nil {
fmt.Println("i is nil") // 表示される
}
if j == nil {
fmt.Println("j is nil") // 表示されない (!)
}
if j.(*A) == nil {
fmt.Println("j is nil") // 表示される
}
j = nil
if j == nil {
fmt.Println("j is nil") // 表示される
}
}
Go言語では、nil
は型の情報を含んでいるようです。そのため、(*A)
型のnil
をinterface{}
型のnil
に変換するとおかしなことになります。
教訓
interface{}
型のnil
チェックでは気をつける
参考
https://qiita.com/umisama/items/e215d49138e949d7f805
https://stackoverflow.com/questions/19761393/why-does-go-have-typed-nil
プリミティブ型(chan
)で変数の自動初期化を頼れない
Go言語では大概の変数は宣言すると同時に初期化されます。しかしながらchan
の場合はどうでしょうか。
func main() {
var ch chan int
go func() {
ch <- 123
}()
fmt.Println(<-ch) // fatal error: all goroutines are asleep - deadlock! と表示
}
解決法
make(chan int)
を用いる
(2019.4.15追記)
chan
型はnil-ableなので、宣言した直後はnil
になっている、との事です。(下のコメント欄参照)
Go Modulesを使うために固有のURLを割り振る必要がある
Go1.11からGO Modulesが導入され、Go言語の標準機能でモジュールを簡単に扱えるようになりました。
Go Modulesを使用するためにはまず以下のようにgo mod init
を実行する必要があります。
$ go mod init https://example.com/testproj
このプロジェクトに対してgo build
を実行すると、testproj
という名前の実行ファイルが生成されます。
問題点は、公開していないプロジェクトでも、modulesを使用するためには何らかのURLを設定しなければならないことです。そういった場合どのようなURLを使えばいいのか、わかる方がいたらぜひ教えていただきたいです。
Goのツールチェーンに--verbose
相当の機能がない
go言語のツールチェーンは、ビルドシステムを含んでいたり、モジュールを扱えるなど様々な機能を持っています。
ツールが様々な面倒を見てくれるのは便利である反面、おかしな挙動に出くわしたときに調べる手間が増えます。
その際、ツールの動作を調べるために--verbose
オプションがあれば便利なのですが、現状用意されていません。よって、最悪Goツールチェインのソースコードまで戻って追う必要に迫られることがあります。
なぜかgoto
が使える
Go言語ではgoto
が使えます。(go
だけに)
エラーハンドリングを一箇所にまとめるときに有用なようです。
参考: https://tmrtmhr.info/tech/why-does-golang-not-have-exceptions/