1. yasuo-ozu

    Posted

    yasuo-ozu
Changes in title
+Go言語は沼
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,631 @@
+Go言語入門者である私が気づいたことを長々と書いています。
+既に他の方が言及されていることも多いです。また初心者でよくわかっていないことも多いためお手柔らかにお願いします。
+
+なお、順番は適当です。
+
+# 継承の代わりとして匿名フィールドを用いた場合、型の判定がうまくいかない
+
+Go言語はオブジェクト指向言語ではありませんが、構造体やレシーバを用いることでオブジェクトのメンバを「呼び出す」ことができます。
+まず、`Animal`「クラス」を作ってみましょう。そして自己紹介するためのレシーバ`Describe()`も定義します。
+
+```go
+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構造体の機能も持たせることができます。
+
+```go
+type Person struct {
+ Animal
+ Name string
+}
+func (person *Person) Describe() {
+ fmt.Printf("I am %v, %v years old.\n", person.Name, person.Age)
+}
+```
+
+ついでに`Plant`(植物)も定義します。植物には口がないため自己紹介はできません。
+
+```go
+type Plant struct {}
+```
+
+ここまでできたら、試しにAnimalとPerson, Plantのオブジェクトをそれぞれ生成して自己紹介させてみましょう。
+
+```go
+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)
+}
+```
+
+実行すると以下のようになります。
+
+```terminal
+I am 10 years old.
+It is not an animal.
+It is not an animal.
+```
+
+動物であるはずの人間が「人間でない」と判定されてしまいました。
+
+## 解決法
+
+対象が動物かどうかを判定するときに、インターフェースを使えばうまくいきます。
+
+```go
+type LooksLikeAnimal interface{
+ Describe()
+}
+func callDescribe(obj interface{}) {
+ switch obj.(type) {
+ case LooksLikeAnimal:
+ (obj.(LooksLikeAnimal)).Describe()
+ default:
+ fmt.Println("It is not an animal.")
+ }
+}
+```
+
+実行結果
+
+```terminal
+I am 10 years old.
+I am Joe, 20 years old.
+It is not an animal.
+```
+
+期待通りに表示されました。
+
+# レシーバを使う際に注意が必要な場合
+
+以下のコードを見てください。
+
+```go
+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()
+}
+```
+
+実行結果は以下のようになります。
+
+```terminal
+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関数を以下のように書き換えて試してみます。
+
+```go
+func main() {
+ var panimal *Animal = nil
+ panimal.DescribeP()
+ panimal.Describe()
+}
+```
+
+出力はこのようになります。
+
+```terminal
+I am a pointer of animal.
+panic: runtime error: invalid memory address or nil pointer dereference
+```
+
+1つ目のポインタレシーバの呼び出しはうまく行きましたが、2つ目の呼び出しは実行時エラーで失敗しました。
+考えてみれば当たり前なのですが、ポインタでないレシーバを使う場合は気をつける必要があるようです。
+
+また以下のような場合はどうでしょうか。
+
+```go
+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言語では配列の宣言及びコピーは以下のようにできます。
+
+```go
+func main() {
+ primes := [6]int{2, 3, 5, 7, 11, 13}
+ var arr [6]int = primes
+ fmt.Println(arr)
+}
+```
+
+しかしながら、最初の`primes`の宣言はすこし冗長です。例えばC言語の場合、配列の宣言と初期化を同時に行う場合は以下のように要素数を省略できます。
+
+```c
+int primes[] = {2, 3, 5, 7, 11, 13};
+```
+
+Go言語で同じことをやるとどうなるでしょうか。
+
+```go
+func main() {
+ primes := []int{2, 3, 5, 7, 11, 13}
+ var arr [6]int = primes
+ fmt.Println(arr)
+}
+```
+
+```terminal
+cannot use primes (type []int) as type [6]int in assignment
+```
+
+エラーになってしまいました。
+
+## 解決法
+
+Go言語で配列宣言時の要素数を省略する場合は`...`を用います。
+
+```go
+func main() {
+ primes := [...]int{2, 3, 5, 7, 11, 13}
+ var arr [6]int = primes
+ fmt.Println(arr)
+}
+```
+
+
+# エラーハンドリング
+
+Go言語には`try`...`catch`のようなエラー処理機構がありません。
+......本当はあるのですが、通常は使うことが推奨されません。
+
+参考: https://dave.cheney.net/2012/01/18/why-go-gets-exceptions-right
+
+よって、エラーは基本的に全て戻り値で返します。例:
+
+```go
+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関数をどのように書き換えればよいでしょうか。
+
+```go
+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()
+}
+```
+
+これは一見正しそうですがコンパイルエラーです。
+
+```terminal
+no new variables on left side of :=
+```
+
+Go言語で`:=`は変数の宣言と代入を同時に行ってくれる演算子ですが、変数は二重に宣言することができないため、二回目に`err := ...`としたところでエラーになります。よって、二回目の`:=`を`=`に書き換えれば良いです。
+
+```go
+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()
+}
+```
+
+しかし、別の解決策もあります。実はこの例では、ファイルとディレクトリの作成順を入れ替えると`:=`を使用したままでもエラーが発生しなくなります。
+
+```go
+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回で済ます方法はないでしょうか。
+
+```go
+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")`とすれば、書き込み先のディレクトリが存在しないためエラーになるはずです。
+
+```terminal
+$ go run test.go
+Success!
+$ ls hoge
+. ..
+$ ls huga
+ls: 'huga' にアクセスできません: そのようなファイルやディレクトリはありません
+```
+
+エラーになりませんでした。しかしながらもちろんファイルは作成されていません。なぜでしょうか? ぜひ考えてみてください。
+
+## 番外編: Go言語で`try...cache`に近いことをやる
+
+先程Go言語には`try...cache`が無いと書きましたが、似た仕組みはあります。それが`panic`です。
+では、`panic`を使用してどエラーを起こすにはどのようにすればよいのでしょうか。
+
+```go
+func errFunc(name string) {
+ panic(name + " does not want to do anything.")
+}
+
+func main() {
+ errFunc("Bob")
+ fmt.Println("Success")
+}
+```
+
+```terminal
+$ go run test.go
+panic: Bob does not want to do anything.
+```
+
+`panic`によってプログラムが強制終了されました。ではこれを`cache`するためにmain関数を書き換えます。
+
+```go
+func main() {
+ defer func() {
+ fmt.Printf("recovered from panic: ")
+ fmt.Println(recover())
+ }()
+ errFunc("Bob")
+ fmt.Println("Success")
+}
+```
+
+```terminal
+$ go run test.go
+recovered from panic: Bob does not want to do anything.
+```
+
+うまく`cache`できています。ですが、`"Success"`が表示されません。それは、main関数全体がいわば`try...catch`の`try`節のようになっているからです。では、`finally`を実現してみましょう。
+
+```go
+func main() {
+ func(){
+ defer func() {
+ fmt.Printf("recovered from panic: ")
+ fmt.Println(recover())
+ }()
+ errFunc("Bob")
+ }()
+ fmt.Println("Success")
+}
+```
+
+```terminal
+$ go run test.go
+recovered from panic: Bob does not want to do anything.
+Success
+```
+
+一応うまくできました。しかし見た目が気持ち悪いですね。実際に使う場合は`try`節の内部のみを別の関数として宣言した方がよさそうです。
+
+# for range
+
+Go言語にも、`foreach`のような構文が用意されています。それが`range`です。
+
+```go
+func main() {
+ for p := range [...]int{2, 3, 5, 7, 11} {
+ fmt.Println(p)
+ }
+}
+```
+
+実行結果:
+
+```terminal
+$ go run test.go
+0
+1
+2
+3
+4
+```
+
+期待した結果にはなりませんでした。正しくは以下のようにします。
+
+```go
+func main() {
+ for _, p := range [...]int{2, 3, 5, 7, 11} {
+ fmt.Println(p)
+ }
+}
+```
+
+実行結果:
+
+```terminal
+$ go run test.go
+2
+3
+5
+7
+11
+```
+
+小さな違いが大きなバグを生む典型例です。
+
+# セミコロン自動挿入による文法制約
+
+Go言語で複雑な計算をしたいとします。あまりにも複雑なため1行で収まらず、下のように途中で改行を入れました。
+
+```go
+func main() {
+ a := 2 + 3 + 5 + 7 + 11
+ + 13 + 17 + 19
+ fmt.Println(a)
+}
+```
+
+```terminal
+$ go run test.go
+# command-line-arguments
+./test.go:7:16: +13 + 17 + 19 evaluated but not used
+```
+
+コンパイルが通りませんでした。しかし、以下のように書き換えるとうまくコンパイルできます。
+
+```go
+func main() {
+ a := 2 + 3 + 5 + 7 + 11 +
+ 13 + 17 + 19
+ fmt.Println(a)
+}
+```
+
+なぜこのようになるかというと、Go言語では各行末に自動的に文の終わりを示す`;`を挿入しているからです。2つ目の例がうまくいったのは、行の末尾が記号で終わる場合は`;`を挿入しない、という簡単なルールによって制御されているからです。~~どこかのes6とは大違いですね。~~(実際はそこまで単純ではないようですが)
+すなわち、単純な制御構文の例でも同じことが起こります。
+
+```go
+func main() {
+ a := 2
+ if a != 1 // コンパイルエラー
+ {
+ fmt.Println("a is not 1")
+ }
+}
+```
+
+~~Tclみたい~~
+
+# 名前空間とパッケージ名
+
+Go言語でプログラムを書くとき、最初に`package main`と記述します。これは、「このファイルは`main`パッケージに属している」という意味ですが、`main`以外のパッケージを作って名前空間を分けたい場合はどのようにすればよいのでしょうか。Go言語ではこのパッケージ名はディレクトリ階層に対応しています。
+
+```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`を代入すると大変な厄介者に...。
+
+```go
+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
+
+<!--
+# 同名のstructを匿名メンバとする場合
+
+# interfaceの中が参照かどうかで区別される
+
+```go
+type A struct {}
+type B struct {
+ A
+}
+func f(it interface{}) {
+ switch it.(type) {
+ case A:
+ fmt.Println("A")
+ case *A:
+ fmt.Println("*A")
+ }
+}
+
+func main() {
+ a := A{}
+ f(a) // Aと表示
+ f(&a) // *Aと表示
+}
+```
+-->
+
+
+
+
+# プリミティブ型(`chan`)で変数の自動初期化を頼れない
+
+Go言語では大概の変数は宣言すると同時に初期化されます。しかしながら`chan`の場合はどうでしょうか。
+
+```go
+func main() {
+ var ch chan int
+ go func() {
+ ch <- 123
+ }()
+ fmt.Println(<-ch) // fatal error: all goroutines are asleep - deadlock! と表示
+}
+```
+
+## 解決法
+
+`make(chan int)`を用いる
+
+
+# Go Modulesを使うために固有のURLを割り振る必要がある
+
+Go1.11からGO Modulesが導入され、Go言語の標準機能でモジュールを簡単に扱えるようになりました。
+Go Modulesを使用するためにはまず以下のように`go mod init`を実行する必要があります。
+
+```terminal
+$ 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/