LoginSignup
37
22

More than 1 year has passed since last update.

Go言語は沼

Last updated at Posted at 2018-12-24

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 panicing 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...catchtry節のようになっているからです。では、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)型のnilinterface{}型の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/

37
22
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
22