バックエンドのプログラミング言語として、Go言語は今後も含めてはやり続ける印象を持っているのですが、自分自身は以前少しだけ業務で触ったことがあるだけでほとんど経験がないので、Tour of Goをやってみることにしました。
Go言語を書く人をGopherというらしいので、Gopherを名乗れるように頑張っていこうと思います。
実際にやってみたソースコードは、GitHubで公開してます。
https://github.com/rinoguchi/go_toor
Goのインストール
こちらからダウンロードする方法もあるのですが、複数のプロジェクトで異なるバージョンを使い分けたりすることも考えると、Go自体のバージョン管理を簡単にできる方が良さそうです。
pyenvやnvmのGo版ともいうべきgoenvというものがあるのでそちらでインストールして行きます。このへんを見ながらやってみようと思います。
goenvのインストール
- インストール
Macなので、homebrewでインストールするのが一番楽っぽいです。brew update brew install goenv
- 環境変数
GOENV_ROOT
を定義し、pathを通すecho 'export GOENV_ROOT="$HOME/.goenv"' >> ~/.bash_profile echo 'export PATH="$GOENV_ROOT/bin:$PATH"' >> ~/.bash_profile
- シェルで
goenv init
が実行されるようにする
これによりGOPATH
を管理したり、オートコンプリートが有効になりますecho 'eval "$(goenv init -)"' >> ~/.bash_profile
-
GOPATH
やGOROOT
をgoenv管理にするecho 'export PATH="$GOROOT/bin:$PATH"' >> ~/.bash_profile echo 'export PATH="$PATH:$GOPATH/bin"' >> ~/.bash_profile
-
.bash_profile
を読み込むsource ~/.bash_profile // or exec $SHELL
Goのインストール
-
インストール済みバージョンの確認
goenv versions > * system (set by /Users/ryoichiinoguchi/.goenv/version)
-
インストール可能なバージョンの確認
goenv install --list > 1.2.2 > 1.3.0 > ... > 1.17.6 > 1.18beta1
- 指定したバージョンのGoをインストール
goenv install 1.17.6
- インストール結果を確認
goenv versions > * system (set by /Users/ryoichiinoguchi/.goenv/version) > 1.17.6
=> インストールはされてるけど、アクティブになってないようです
-
グローバルで、
1.17.6
をアクティブにする# 設定 goenv global 1.17.6 # 確認 goenv versions > system > * 1.17.6 (set by /Users/ryoichiinoguchi/.goenv/version)
無事、グローバルでアクティブになりました。
- 特定フォルダ配下だけ、別バージョンを利用する
# 特定フォルダに移動 cd test # ローカル設定 .go-versionが作られる goenv local 1.11.4 # 確認 goenv versions system * 1.11.4 (set by /Users/ryoichiinoguchi/Desktop/test/.go-version) 1.17.6
無事、特定フォルダ配下だけ別のバージョンがアクティブになりました。
別のフォルダに移動すると、globalのバージョンが有効になります。
Go自体のインストールは終わりました。特に難しくなかったですね。
次は、公式サイトのTour of Goをやっていきたいと思います。
エディタ準備
Webサイト上で実装・実行できるのですが、.go
ファイルを作成して、コピペせずに写経していこうと思います。
そのためにはエディタが必要ですが、自分は普段からよく使っているVSCodeを使おうと思います。
(他にGoLandやATOMなどもあるようです。)
- VSCode拡張
- Goという拡張をインストールします
- Go: Install/Update Tools
Command+Shift+p
で「Go: Install/Update Tools」を入力して出てきたツールを全部インストールします
- ファイル保存時の自動フォーマット設定
.vscode/settings.jsonを作成します{ "editor.formatOnSave": true, "[go]": { "editor.defaultFormatter": "golang.go" } }
これで最低限の開発環境が準備できたと思います。
基礎編
ここからは、Tour of Goをやっていきたいと思います。
最初は、基礎編17ページ分です。
Hello World
-
hello.go
package main import "fmt" func main() { fmt.Println("Hello World!!!") }
- 説明
-
main
パッケージのmain
関数が実行されます
-
- 動作確認
go run basics/hello.go > Hello World!!!
パッケージ
-
packages.go
package main import ( "fmt" "math/rand" "time" ) func main() { rand.Seed(time.Now().UnixNano()) fmt.Println("My favorite number is", rand.Intn(10)) }
- 説明
- プログラムはmainパッケージから開始される
-
fmt
パッケージとmath/rand
パッケージとtime
パッケージがインポートされている - パッケージ名はimportパスの最後の要素と同じになる
-
math/rand
はpackage rand
ステートメントで始まるファイル群を示す - デフォルトだと
rand.Seed(1)
が設定されているため、rand.Intn()
が常に同じになる。それを避けるためにrand.Seed()
に現在のunixタイムを与えている
- 動作確認
go run packages.go > My favorite number is 5 go run packages.go > My favorite number is 7
インポート
-
実装
// good import ( "fmt" "math" ) // bad import "fmt" import "math"
- 説明
-
()
でグループ化してimportするのが良いスタイルらしい - このスタイルを、
factored import statement
という
-
可視性
-
exported-names.go
package main import ( "fmt" "math" ) func main() { // fmt.Println(math.pi) // => error fmt.Println(math.Pi) }
- 説明
- Goでは可視性が先頭文字の大文字/小文字で区別される
-
math.Pi
は先頭が大文字なので外部からアクセスできる(public) -
math.pi
は先頭が小文字なので外部からアクセスできない(private)
- 動作確認
go run exported-names.go > 3.141592653589793
関数
-
functions.go
package main import "fmt" func add(x int, y int) int { return x + y } func main() { fmt.Println(add(12, 34)) }
- 説明
- privateな関数なので、先頭小文字で
add
という関数を定義している - 引数を取ることができ、変数名の後ろに型を記載する
- 戻り値にも型を記載する
- privateな関数なので、先頭小文字で
- 動作確認
go run functions.go > 46
まとめて型指定
-
functions-continued.go
package main import ( "fmt" "strconv" ) func add(x, y int, z string) int { i, _ := strconv.Atoi(z) return x + y + i } func main() { fmt.Println(add(12, 34, "56")) }
- 説明
- 引数の型が連続して同じ場合は、最後にまとめて型を記載できる
- 今回は、最初の二つがint、最後の一つがstringという型を指定している
- 動作確認
go run functions-continued.go > 102
複数の戻り値
-
multiple-results.go
package main import "fmt" func swap(x, y, z string) (string, string, string) { return z, y, x } func main() { a, b, c := swap("!!", "world", "hello") fmt.Println(a, b, c) }
- 説明
- 関数は複数の戻り値を返すことができる
- 動作確認
go run multiple-results.go > hello world !!
Named return values
-
named-results.go
package main import "fmt" func divide(num, denominator int) (quotient, remainder int) { quotient = num / denominator remainder = num % denominator return } func main() { fmt.Println(divide(35, 8)) }
- 説明
- 戻り値となる変数に名前をつけて、
return
ステートメントに何も書かない(naked return)ことができる - 戻り値の名前を明示することができるので、ドキュメントとして使える
- naked returnは短い関数のみで利用すべき(長い関数だとreadableじゃなくなる)
- 戻り値となる変数に名前をつけて、
- 動作確認
go run named-results.go > 4 3
変数
-
variables.go
package main import "fmt" var c, python, java bool func main() { var i int fmt.Println(i, c, python, java) }
- 説明
-
var
で変数を宣言できる - 複数まとめて宣言もできる
- パッケージでも関数でも利用できる
-
- 動作確認
go run variables.go > 0 false false false
変数の初期化
-
variables-with-initializer.go
package main import "fmt" var i, j int = 1, 2 func main() { var c, python, java = true, false, "no!" fmt.Println(i, j, c, python, java) }
- 説明
- 変数名毎に初期化できる
- まとめて初期化することもできる
- 初期化している場合は、型の省略ができる
- 動作確認
go run variables-with-initializers.go > 1 2 true false no!
短い変数宣言
-
short-variable-declarations.go
package main import "fmt" // x := 3 => error func main() { i, j := 1, 2 c, python := true, false fmt.Println(i, j, c, python) }
- 説明
-
var
の代わりに:=
を使って代入することで、暗黙的な型宣言ができる - 関数の外側では
:=
を使って暗黙的な宣言を行うことはできない
-
- 動作確認
go run short-variable-declarations.go > 1 2 true false
基本型(組み込み型)
-
basic-types.go
package main import ( "fmt" "math/cmplx" ) var ( ToBe bool = false MaxInt uint64 = 1<<64 - 1 z complex128 = cmplx.Sqrt(-5 + 12i) ) func main() { fmt.Printf("type: %T, value: %v\n", ToBe, ToBe) fmt.Printf("type: %T, value: %v\n", MaxInt, MaxInt) fmt.Printf("type: %T, value: %v\n", z, z) }
- 説明
- 以下のような基本型が存在する
-
bool
// 真偽値 -
string
// 文字列 -
int
int8
int16
int32
int64
// 符号あり整数 -
uint
uint8
uint16
uint32
uint64
uintptr
// 符号なし整数 -
byte
// uint8と同じ -
rune
// int32と同じ -
float32
float64
// 浮動小数点 -
complex64
complex128
// 複素数
-
-
int
uint
uintptr
は、32bitシステムでは32bit、64bitシステムでは64bit - 特別な理由がなければ、整数は
int
を利用するべき
- 以下のような基本型が存在する
- 動作確認
go run basic-types.go > type: bool, value: false > type: uint64, value: 18446744073709551615 > type: complex128, value: (2+3i)
初期値
-
zero.go
package main import "fmt" func main() { var i int var f float64 var b bool var s string fmt.Printf("%v %v %v %v\n", i, f, b, s) fmt.Printf("%v %v %v %q\n", i, f, b, s) }
- 説明
- 初期化すると、ゼロ値が与えられる
- 数値型(
int
,float
など): 0 - bool型: false
- string型: ""(空文字)
- 数値型(
- 初期化すると、ゼロ値が与えられる
- 動作確認
go run zero.go > 0 0 false > 0 0 false ""
型変換
-
type-conversions.go
package main import ( "fmt" "math" ) func main() { var x, y int = 4, 7 var f float64 = math.Sqrt(float64(x*x + y*y)) var z uint = uint(f) var s string = fmt.Sprint(z) // var s string = string(z) => not working expectedly fmt.Println(x, y, f, z, s) }
- 説明
-
int(x)
やfloat64(x)
のように型名で型変換ができる - ただし、
string(x)
は期待通り動かず、fmt.Sprint(x)
を利用することでstring型に変換できる
-
- 動作確認
go run type-conversions.go > 4 7 8.06225774829855 8 8
型推論
-
type-inference.go
package main import "fmt" func main() { i := 42 f := 42.123 b := true i2 := i fmt.Printf("type of i is %T\n", i) fmt.Printf("type of f is %T\n", f) fmt.Printf("type of b is %T\n", b) fmt.Printf("type of i2 is %T\n", i2) }
- 説明
- 左辺の変数は、右辺の変数や値から型推論される
- 動作確認
go run type-inference.go > type of i is int > type of f is float64 > type of b is bool > type of s is string > type of i2 is int
定数
-
constants.go
package main import "fmt" const pi = 3.14 func main() { const world = "World" fmt.Println("Hello", World) radius := 4.00 fmt.Printf("Area of a circle with radius %v is %v\n", radius, radius*radius*pi) }
- 説明
- 定数は
const
キーワードを使って宣言する - 定数は、文字、文字列、bool値、数値でのみ使える
- 定数は
- 動作確認
go run constants.go > Hello World > Area of a circle with radius 4 is 50.24
数値の定数
-
numeric-constants.go
package main import "fmt" const ( big = 1 << 100 // 1267650600228229401496703205376 small = big >> 99 // ) func needInt(x int) int { return x*10 + 1 } func needFloat(x float64) float64 { return x * 0.1 } func main() { fmt.Println(small) // fmt.Println(big) => error: cannot use big (untyped int constant 1267650600228229401496703205376) as int value in argument to fmt.Println (overflows) fmt.Println(needInt(small)) // fmt.Println(needInt(big)) => error: cannot use big (untyped int constant 1267650600228229401496703205376) as int value in argument to fmt.Println (overflows) fmt.Println(needFloat(small)) fmt.Println(needFloat(big)) }
- 説明
- 数値の定数は高精度な値
- 型のない定数は、状況によって必要な型を取る
- 定数
big
はint
の最大値を超えるため、int
として振る舞うことはできず、needInt(big)
はエラーになる - 定数
small
はint
としてもfloat
としても振る舞うことができるので、needInt(small)
もneedFloat(small)
もそれぞれ必要な型で動作する
- 動作確認
go run numeric-constants.go > 2 > 21 > 0.2 > 1.2676506002282295e+29
これで、基礎編は終わりです。
ここまでは特に難しいところはなくて、非常にわかりやすかったですね。
Tour of Go: 制御構文
次は、制御構文をやってみようと思います。
思ったより先が長いので、複数ページ適宜まとめたりスキップしたりしつつ進めようと思います^^
for文
-
for.go
package main import "fmt" func main() { simple() withoutPrePostProcess() infinitLoop() } // シンプルなfor文 func simple() { sum := 1 for i := 0; i < 10; i++ { sum += i } fmt.Println("simple", sum) } // 初期化、後処理なし func withoutPrePostProcess() { sum := 1 for sum < 100 { sum += sum } fmt.Println("without pre post process", sum) } // 初期化、後処理なし func infinitLoop() { sum := 1 for { sum += sum if sum > 100000000 { break } } fmt.Println("loop", sum) }
- 説明
-
for 初期化: 条件: 後処理 {}
の形式 - 初期化と後処理は省略可能
- 条件を省略すると無限ループになる
-
break
やcontinue
もできる
-
- 動作確認
go run for.go > simple 46 > without pre post process 128 > loop 134217728
if文
-
if.go
package main import ( "fmt" "math" ) func main() { fmt.Println(sqrt(10.0)) fmt.Println(sqrt(-10.0)) fmt.Println(pow(2, 4)) fmt.Println(pow(2, 8)) fmt.Println(pow(2, 12)) } func sqrt(v float64) string { // simple if if v < 0 { return sqrt(-v) + "i" } return fmt.Sprint(math.Sqrt(v)) } func pow(x, n float64) string { // if with initialize if v := math.Pow(x, n); v < 100 { return fmt.Sprintf("%v: less than 100", v) } else if v < 1000 { return fmt.Sprintf("%v: less than 1000", v) } else { return fmt.Sprintf("%v: over 1000", v) } }
- 説明
-
if
-else if
-else
が使える - if文の条件の前で、変数を宣言できる。また、その変数は
else-if
やelse
ブロックで参照できる
-
- 動作確認
go run if.go > 3.1622776601683795 > 3.1622776601683795i > 16: less than 100 > 256: less than 1000 > 4096: over 1000
練習問題: for / if
- 問題
- 平方根を計算する
-
exercise.go
package main import ( "fmt" "math" ) func Sqrt(x float64) float64 { z := 1.0 for i := 1; i < 100; i++ { newZ := z - (z*z-x)/(2*z) fmt.Println(i, ":", newZ) if math.Abs(newZ-z) < 0.00000000001 { break } z = newZ } return z } func main() { fmt.Println("result:", Sqrt(2)) fmt.Println("result:", Sqrt(3)) fmt.Println("result:", Sqrt(4)) }
- 説明
-
z*z-x
の部分はある数値z
を二乗した値と二乗の期待値x
との差分を示している - この差分を
2z
で割った値をもとのz
から引いた値は、もとのz
よりも期待値に近づいている(ニュートン法) -
z
の開始値は1.0
とし、ニュートン法で少しずつz
の値を変え、誤差が0.00000000001
より小さくなった場合に、値を返却している
-
- 動作確認
go run exercise.go > 1 : 1.5 > 2 : 1.4166666666666667 > 3 : 1.4142156862745099 > 4 : 1.4142135623746899 > 5 : 1.4142135623730951 > result: 1.4142135623746899 > 1 : 2 > 2 : 1.75 > 3 : 1.7321428571428572 > 4 : 1.7320508100147276 > 5 : 1.7320508075688772 > 6 : 1.7320508075688774 > result: 1.7320508075688772 > 1 : 2.5 > 2 : 2.05 > 3 : 2.000609756097561 > 4 : 2.0000000929222947 > 5 : 2.000000000000002 > 6 : 2 > result: 2.000000000000002
=> どれも、5〜6回ぐらいで期待する精度の平方根を返却することができている
switch文
-
switch.go
package main import ( "fmt" "runtime" "time" ) func main() { printOs() printHour() } func printOs() { switch os := runtime.GOOS; os { case "darwin": fmt.Println("OS X.") case "linux": fmt.Println("Linux.") default: fmt.Printf("%s.\n", os) } } func printHour() { hour := time.Now().Hour() switch { case hour < 12: // 式を記載することもできる fmt.Println(hour, "AM") default: fmt.Println(hour-12, "PM") } }
- 説明
- Goではマッチした
case
だけが実行され、後続のcase
は実行されないので、ので、break
は不要(便利!) - if文と同様に評価する変数の初期化を行うことができる
-
switch
に変数を指定しない場合、case
に式を記載することができる
- Goではマッチした
- 動作確認
go run switch.go > OS X. > 0 PM
Defer
-
defer.go
package main import "fmt" func main() { // main()のreturnのタイミングまで実行を遅延 defer fmt.Print("太郎 ") defer fmt.Print("次郎 ") defer fmt.Print("三郎 ") fmt.Print("hello ") }
- 説明
-
defer
に渡した関数の呼び出しは、呼び出し元のreturn
のタイミングまで実行が遅延される - 複数回
defer
した場合、呼び出しはスタックされ、後入れ先出し(LIFO)の順番で実行される
-
- 動作確認
go run defer.go > hello 三郎 二郎 太郎
=> 後にスタックに追加された順(三郎、二郎、太郎の順)に実行されていることがわかる
これでこの章は終わりです。まだまだ、先は長く終わったのは全体の1/3ぐらいな気がします。。
いろんな型
構造体、配列、slice、mapなどの型についてのチュートリアルです。
やっとGoらしくなってきました。
ポインタ(pointer)
Goはポインタを扱います。ポインタはメモリアドレスを指すものです。
-
pointers.go
package main import "fmt" func main() { i := 100 var p *int = &i // p := &i と同等 fmt.Println(p) fmt.Println(*p) *p = 200 fmt.Println(p) fmt.Println(*p) *p = *p + 100 fmt.Println(p) fmt.Println(*p) }
- 説明
-
int
型の変数のポインタは*int
型となる - 変数
i
のポインタは、&
オペレータを利用して&i
で示す - ポインタ
p
の指す変数は、*
オペレータを利用して*p
で示す - ポインタは使い所を意識して使うのが良さそう
- こちらの記事がわかりやすかったです
-
- 動作確認
go run pointers.go > 0xc00010c008 > 100 > 0xc00010c008 > 200 > 0xc00010c008 > 300
=> ポインタ自体はメモリアドレスで、ポインタの指す変数の値を変更しても変わっていないのが分かる
構造体(struct)
-
struct.go
package main import "fmt" type Physique struct { Height int Weight int } type Human struct { Name string Physique Physique } func main() { fmt.Println("==========taro==========") taroPhysique := Physique{Height: 170, Weight: 60} taro := Human{Name: "taro", Physique: taroPhysique} fmt.Println("taro:", taro) fmt.Println("taro.Name:", taro.Name) fmt.Println("taro.Physique:", taro.Physique) taroP := &taro fmt.Println("taroP:", taroP) fmt.Println("taroP.Name:", taroP.Name) fmt.Println("*taroP.Name:", taroP.Name) fmt.Println("==========jiro==========") jiro := Human{Name: "jiro", Physique: Physique{Height: 180, Weight: 90}} fmt.Println("jiro:", jiro) fmt.Println("==========saburo==========") saburo := Human{Name: "saburo"} fmt.Println("saburo:", saburo) saburo.Physique.Height = 200 saburo.Physique.Weight = 100 fmt.Println("saburo:", saburo) }
- 説明
- 構造体はフィールドの集まり。ネストすることもできる
-
taro.Physique.Height
のように.
で参照でき、値を設定することもできる - 構造体のポインタを通じて、フィールドにアクセスする場合、
*taroP.Name
のように書くこともできるが大変なので、taroP.Name
のように記載することもできる - 初期化時に指定しなかったフィールドはゼロ値が設定される
- ネストされた構造体の初期化時は大変
- stack overflow参照
- 上記サンプルの書き方がベストっぽい
- 動作確認
go run struct.go > ==========taro========== > taro: {taro {170 60}} > taro.Name: taro > taro.Physique: {170 60} > taroP: &{taro {170 60}} > taroP.Name: taro > *taroP.Name: taro > ==========jiro========== > jiro: {jiro {180 90}} > ==========saburo========== > saburo: {saburo {0 0}} > saburo: {saburo {200 100}}
固定長配列(array)
-
array.go
package main import "fmt" func main() { var a [2]string a[0] = "Hello" a[1] = "World" fmt.Println("a:", a) fmt.Println(a[0]) fmt.Println(a[1]) // fmt.Println(a[2]) // out of bounds error b := [2]string{"hello", "world"} fmt.Println("b:", b) }
- 説明
- Goの
array
は固定長。使い所は限られる - 例えば、
[2]string
は文字列2個の配列を表す - 通常は、可変長の
slice
を利用する
- Goの
- 動作確認
go run array.go > a: [Hello World] > Hello > World > b: [hello world]
可変長配列(slice)
-
sclice.go
package main import "fmt" func main() { // sliceの初期化 brothers := []string{"taro", "jiro", "saburo", "siro", "goro"} fmt.Println("brothers:", brothers) fmt.Println("len=", len(brothers), "cap=", cap(brothers)) // appendした時のlenとcap確認 brothers = append(brothers, "rokuro", "shichiro") fmt.Println("brothers:", brothers) fmt.Println("len=", len(brothers), "cap=", cap(brothers)) // サブsliceを取り出した時のlenとcap確認 brothers = brothers[0:3] fmt.Println("brothers:", brothers) fmt.Println("len=", len(brothers), "cap=", cap(brothers)) // zero slice var zeroSlice []int fmt.Println("zeroSlice", zeroSlice) fmt.Println("len=", len(zeroSlice), "cap=", cap(zeroSlice)) fmt.Println("zeroSlice == nil:", zeroSlice == nil) // makeでlenとcapを指定して初期化 sisters := make([]string, 2, 5) fmt.Println("sisters:", sisters) fmt.Println("len=", len(sisters), "cap=", cap(sisters)) sisters[0] = "ichiko" sisters[1] = "niko" // sisters[2] = "sanko" index out of range error fmt.Println("sisters:", sisters) fmt.Println("len=", len(sisters), "cap=", cap(sisters)) }
- 説明
-
array
で個数を指定しないことで、slice
型となる -
brothers[0:3]
のような感じで、配列の一部から新しいslice
を作成することができる - 要素数
len
と容量cap
が定義されている- 容量は確保しているメモリ領域。
append
するとappend
した要素数よりも多めに確保される
- 容量は確保しているメモリ領域。
-
slice
のゼロ値はnil
で、要素数、容量は0
-
make
を使って、要素数と容量を指定して初期化することもできる- シビアな処理では、メモリの再確保などが発生しないように
-
- 動作確認
go run slice.go > brothers: [taro jiro saburo siro goro] > len= 5 cap= 5 > brothers: [taro jiro saburo siro goro rokuro shichiro] > len= 7 cap= 10 # appendしたら要素数が2と容量が5増えた > brothers: [taro jiro saburo] > len= 3 cap= 10 # sub sliceの容量はもとのsclieの値 > zeroSlice [] > len= 0 cap= 0 > zeroSlice == nil: true # zero slice はnil > sisters: [ ] > len= 2 cap= 5 # makeで要素数、容量を指定 > sisters: [ichiko niko] > len= 2 cap= 5
Range
-
range.go
package main import "fmt" func main() { brothers := []string{"taro", "jiro", "saburo"} for i, v := range brothers { fmt.Println(i, v) } for _, v := range brothers { fmt.Println(v) } }
- 説明
-
range
はfor
文で利用するもので、slice
やmap
のindexと要素の二つの変数を返す -
_
へ代入することで、捨てることもできる
-
- 動作確認
go run range.go > 0 taro > 1 jiro > 2 saburo > taro > jiro > saburo
練習問題: slice
- 問題
- 長さ
dy
のslice
に、各要素が8bitのunsigned int
型で長さdx
のslice
を割り当てたものを返すPic
関数を実装する
- 長さ
-
exercise-slice.go
package main import "golang.org/x/tour/pic" func Pic(dx, dy int) [][]uint8 { s := make([][]uint8, dy) for y := 0; y < dy; y++ { s[y] = make([]uint8, dx) for x := 0; x < dx; x++ { s[y][x] = uint8(x * y) // ここは色々変えてみると、イメージが変わって面白い } } return s } func main() { pic.Show(Pic) }
- 説明
-
golang.org/x/tour/pic
というパッケージを使ってることに注意 - 外部モジュールの解決
go mod init exercise-slice go mod tidy # => go.modとgo.sumが作成される
-
- 動作確認
go run exercise-slice.go > IMAGE:iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAACAAElEQVR42uy9B3jUVfb/P7r21dVVdy1r74oFWRAiLZDe+2SS6ZWhhBZ6zdJC72XG6RkQkQUEBKQjEFoICSWE3pFiQUV3Lav+n/f/nmdu7odPArs/y+6Xj8997vM6534mmHvP+55zZhK4SaX6SaX8p/x3vf53k0qluu(省略)
=> base64円コーディングした画像データがログに出力されている
=><img src="data:image/png;base64, xxx">
のxxx
の部分に相当する内容
=> これはブラウザで表示しないと意味がわからないので、素直にWebページ上で実行した方がよさそう
Map
-
map.go
package main import "fmt" func main() { // makeを使って初期化 var m1 map[string]int m1 = make(map[string]int) m1["taro"] = 170 m1["jiro"] = 180 fmt.Println("m1:", m1) // makeを使わず初期化 m2 := map[string]int{ "saburo": 190, "shiro": 200, } fmt.Println("m2:", m2) // 要素を取得 fmt.Println("m2[\"saburo\"]:", m2["saburo"]) // 要素の削除 delete(m2, "shiro") fmt.Println("m2:", m2) // 要素の追加 m2["goro"] = 210 fmt.Println("m2:", m2) // 要素の存在確認 elem, ok := m2["goro"] fmt.Println(elem, ok) elem, ok = m2["rokuro"] fmt.Println(elem, ok) }
- 説明
-
map
もslice
を使ってサイズ指定して生成することも、直接初期化することもできる - 要素の存在確認方法がやや独特。
-
- 動作確認
go run map.go > m1: map[jiro:180 taro:170] > m2: map[saburo:190 shiro:200] > m2["saburo"]: 190 > m2: map[saburo:190] > m2: map[goro:210 saburo:190] > 210 true > 0 false
練習問題: map
- 問題
- 文章中の単語毎の出現回数をカウントして、
map
で返す
- 文章中の単語毎の出現回数をカウントして、
-
exercise-map.go
package main import ( "strings" "golang.org/x/tour/wc" ) func WordCount(s string) map[string]int { wordCounts := make(map[string]int) words := strings.Split(s, " ") // 単語に分割 for _, word := range words { wordCounts[word] += 1 // 存在しない場合はゼロ値(=0)が返るので、`+= 1`でOK } return wordCounts } func main() { wc.Test(WordCount) }
- 注意点
- ローカルPC上で動かす際は、外部パッケージのインストールが必要
# go.mod / go.sum を作成 go mod init exercise-map # そーすこーどから必要なパッケージを探して go.mod / go.sum に記載した上で、インストール go mod tidy
- ローカルPC上で動かす際は、外部パッケージのインストールが必要
- 動作確認
go run exercise-map.go > PASS > f("I am learning Go!") = > map[string]int{"Go!":1, "I":1, "am":1, "learning":1} > PASS > f("The quick brown fox jumped over the lazy dog.") = > map[string]int{"The":1, "brown":1, "dog.":1, "fox":1, "jumped":1, "lazy":1, "over":1, "quick":1, "the":1} > PASS > f("I ate a donut. Then I ate another donut.") = > map[string]int{"I":2, "Then":1, "a":1, "another":1, "ate":2, "donut.":2} > PASS > f("A man a plan a canal panama.") = > map[string]int{"A":1, "a":2, "canal":1, "man":1, "panama.":1, "plan":1}
クロージャ
Goの関数は変数なので、他の変数と同じように引数/戻り値として関数と受け渡しできる(=第一級関数)。
関数は、自身が定義されたスコープ(静的スコープ、レキシカルスコープ)の変数を参照することができ、これを参照するような関数をクロージャと呼ぶ(たぶん)。
-
closure.go
package main import "fmt" func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } func main() { s := adder() for i := 0; i < 10; i++ { fmt.Println(s(i)) } }
- 説明
- 関数
adder()
はsum
という変数を持ち、sum
に値を加算する無名関数を返却する -
main()
はadder()
を呼び出すことで無名関数のオブジェクトを受け取り、0
〜9
までの数値を引数に10回無名関数を実行する - 無名関数は、自身が定義された静的スコープの変数
sum
に変数に順番に値を加算している - もちろん、
main()
から変数sum
を参照することはできない
- 関数
- 動作確認
go run closure.go > 0 > 1 > 3 > 6 > 10 > 15 > 21 > 28 > 36 > 45
=> 静的スコープの変数
sum
の値が加算されていることが確認できる
練習問題: closure
- 問題
- fibonacci数をprintする関数を実装する
-
fibonacci.go
package main import "fmt" func fibonacci() func() int { x := 0 y := 0 return func() int { if y == 0 { x, y = 0, 1 } else { x, y = y, x+y } return x } } func main() { f := fibonacci() for i := 0; i < 10; i++ { fmt.Println(f()) } }
- 説明
- フィボナッチ数列は、
F(n+2) = F(n) + F(n+1)
という漸化式で定義される- 直近2つの数値の合計が次の値になるような数列
- 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987...
- フィボナッチ数列は、
- 動作確認
go run fibonacci.go > 0 > 1 > 1 > 2 > 3 > 5 > 8 > 13 > 21 > 34
=> 問題なくフィボナッチ数がプリントされているようです
少し時間はかかりましたが、「いろいろな型」がやっと終わりました。
メソッドとインターフェース
メソッド
GoにはClassはありませんが、型にメソッドを定義できます。
-
methods.go
package main import ( "fmt" ) // 長方形 type Rectangle struct { X, Y float64 } // 面積計算 値レシーバ func (r Rectangle) Area() float64 { return r.X * r.Y } // 面積計算 ポインタレシーバ func (r *Rectangle) AreaR() float64 { return r.X * r.Y } // 拡大縮小 値レシーバ func (r Rectangle) Scale(f float64) { r.X = r.X * f r.Y = r.Y * f } // 拡大縮小 ポインタレシーバ func (r *Rectangle) ScaleR(f float64) { r.X = r.X * f r.Y = r.Y * f } func main() { fmt.Println("===== 値レシーバ × 値 =====") r1 := Rectangle{3.0, 5.0} fmt.Printf("area: %v\n", r1.Area()) r1.Scale(2) fmt.Printf("area: %v\n", r1.Area()) fmt.Println("===== ポインタレシーバ × 値 =====") r2 := Rectangle{3.0, 5.0} fmt.Printf("area: %v\n", r2.AreaR()) r2.ScaleR(2) fmt.Printf("area: %v\n", r2.AreaR()) fmt.Println("===== 値レシーバ × ポインタ =====") p1 := &Rectangle{3.0, 5.0} fmt.Printf("area: %v\n", p1.Area()) p1.Scale(2) fmt.Printf("area: %v\n", p1.Area()) fmt.Println("===== ポインタレシーバ × ポインタ =====") p2 := &Rectangle{3.0, 5.0} fmt.Printf("area: %v\n", p2.AreaR()) p2.ScaleR(2) fmt.Printf("area: %v\n", p2.AreaR()) }
- 説明
- メソッドはレシーバ引数を受け取ることができて、レシーバに対してメソッドを生やすようなことができる
- レシーバ引数には任意の型を受け取ることができる(
struct
だけでなく、float64
などでもOK)
- レシーバ引数には任意の型を受け取ることができる(
- レシーバ引数には、値(インスタンス)とポインタの二種類を指定することができる
- 値レシーバ
- メソッドに引数を渡す際に値はコピーされるため、値を変更しても元の変数は変更されない!
- => メソッド内で値を変更しない場合に利用する(副作用が起きない)
- ポインタレシーバ
- ポインタが渡されるので、値を変更したら元の変数も変更される!
- 値コピーが発生しないので、巨大な構造体を処理する際などは高速
- => メソッド内で値を変更する場合、巨大な構造体の場合に利用する
- メソッドはレシーバ引数を受け取ることができて、レシーバに対してメソッドを生やすようなことができる
- 動作確認
go run methods.go > ===== 値レシーバ × 値 ===== > area: 15 > area: 15 # 値は変更されてない > ===== ポインタレシーバ × 値 ===== > area: 15 > area: 60 # 値は変更されている > ===== 値レシーバ × ポインタ ===== > area: 15 > area: 15 # 値は変更されてない > ===== ポインタレシーバ × ポインタ ===== > area: 15 > area: 60 # 値は変更されている
インターフェース
-
interfaces.go
package main import "fmt" type Greeter interface { Greet() Type() string } type Human struct { Name string } type Monkey struct { Name string } func (human Human) Greet() { fmt.Printf("Hello, my name is %v.\n", human.Name) } func (human Human) Type() string { return "Human" } func (monkey Monkey) Greet() { fmt.Printf("Wookey, wookakey wookkkey ukkety %v.\n", monkey.Name) } func (monkey Monkey) Type() string { return "Monkey" } func main() { var greeter1 Greeter = Human{"Taro"} println(greeter1.Type()) greeter1.Greet() var greeter2 Greeter = Monkey{"Jiro"} println(greeter2.Type()) greeter2.Greet() }
- 説明
- インターフェースは、メソッドのシグチャの集まり
-
implements
のようなキーワードは不要で、型にメソッドを実装していき、インターフェースに定義されている全てのメソッドを実装したら、コンパイルエラーが発生しなくなる - インターフェース名はメソッド名+
er
みたい名前にするらしい
- 動作確認
go run interfaces.go > Human > Hello, my name is Taro. > Monkey > Wookey, wookakey wookkkey ukkety Jiro.
空のインターフェース
-
empty-interface.go
package main import "fmt" func printType(i interface{}) { switch i.(type) { case int: fmt.Println("this is int") case string: fmt.Println("this is string") default: fmt.Println("this is other") } } func main() { fmt.Println("=========== 基本 ===========") var unknown1 interface{} fmt.Printf("%v, %T\n", unknown1, unknown1) unknown1 = "ABC" fmt.Printf("%v, %T\n", unknown1, unknown1) unknown1 = 123 fmt.Printf("%v, %T\n", unknown1, unknown1) fmt.Println("=========== 型チェック ===========") var unknown2 interface{} str, ok := unknown2.(string) fmt.Println(str, ok) unknown2 = "ABC" str, ok = unknown2.(string) fmt.Println(str, ok) unknown2 = 123 str, ok = unknown2.(string) fmt.Println(str, ok) // str = unknown2.(string) // => `panic: interface conversion: interface {} is int, not string` occurrs fmt.Println("=========== 型スイッチ ===========") var unknown3 interface{} printType((unknown3)) unknown3 = "ABC" printType((unknown3)) unknown3 = 123 printType((unknown3)) }
- 説明
- 未知の型を扱いたい場合、
interface{}
を利用する -
fmt.Printf("%T")
で型を出力できる - 別の変数に型を指定して代入すると、二番目の変数で型を満たしているかどうかを返してくれる
- 二番目の変数を指定しない場合、型が間違っていると
panic
が起きる
- 二番目の変数を指定しない場合、型が間違っていると
-
switch
でi.(type)
を使って、型によって処理を切り替えることができる-
i.(type)
はswitch
以外では使えない
-
- 未知の型を扱いたい場合、
- 動作確認
go run empty-interfaces.go > =========== 基本 =========== > <nil>, <nil> > ABC, string > 123, int > =========== 型チェック =========== > false > ABC true > false > =========== 型スイッチ =========== > this is other > this is string > this is int
練習問題: stringers
文字列を扱うstringers
というよく使われるインターフェスがあるので、それの練習問題をやってみます。
- 問題
-
[]byte
形式で渡されるIPアドレスを、1.2.3.4
の形式に変換するString()
メソッドを作成する
-
-
stringers.go
package main import "fmt" type IPAddr [4]byte func (ip IPAddr) String() string { return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[3], ip[3]) } func main() { hosts := map[string]IPAddr{ "loopback": {127, 0, 0, 1}, "googleDNS": {8, 8, 8, 8}, } for name, ip := range hosts { fmt.Printf("%v: %v\n", name, ip) } }
* 説明
- 本当は`strings.Join()`とかを使いたかったが、`[]byte`を`[]string`に変換するのが大変なので、`fmt.Sprintf()`を利用している
- 配列に対して`map()`みたいなことが簡単にできないのが手間に感じる...
- 本当は`strings.Join()`とかを使いたかったが、`[]byte`を`[]string`に変換するのが大変なので、`fmt.Sprintf()`を利用している
- 動作確認
go run stringers.go > loopback: 127.0.1.1 > googleDNS: 8.8.8.8
エラー
-
errors.go
package main import ( "fmt" "time" ) type MyError struct { Time time.Time Reason string } func (e *MyError) Error() string { return fmt.Sprintf("%s: %v", e.Reason, e.Time) } func calsSum(a int, b int) (int, error) { sum := a + b if sum > 100 { return 0, &MyError{time.Now(), "limit over"} } return sum, nil } func main() { var err error var sum int fmt.Println("========== normal case ==========") sum, err = calsSum(10, 80) if err != nil { fmt.Println(err) return } fmt.Println(sum) fmt.Println("========== error case ==========") sum, err = calsSum(50, 80) if err != nil { fmt.Println(err) return } fmt.Println(sum) }
- 説明
- `error`インターフェースは以下のようにエラーの文字列表現を返す`Error()`メソッドが定義されている
```go
type error interface {
Error() string
}
``` - 例では、`MyError`という型を作って、この方でエラーの内容を管理し、`Error()`メソッドを実装している
- Goでは、エラーをthrowするのではなく、戻り値として返却する
- 呼び出し元はエラーが`nil`かどうかを判定し、エラー処理を行う
- `error`インターフェースは以下のようにエラーの文字列表現を返す`Error()`メソッドが定義されている
- 動作確認
go run errors.go ========== normal case ========== 90 ========== error case ========== limit over: 2022-03-06 18:14:03.881209 +0900 JST m=+0.000138684
練習問題: エラー
- 問題
- 平方根(ルート)を計算する関数にて、負の数が指定されたらエラーを返すようにする
-
errors-exercise.go
package main import ( "fmt" "math" ) type ErrNegativeSqrt float64 func (e ErrNegativeSqrt) Error() string { return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e)) } func Sqrt(x float64) (float64, error) { if x
- 解説
- `Sqrt()`というメソッドは以前の練習問題と同じ
- 負の数が与えられた場合、アーリーリターンで`ErrNegativeSqrt`を返している
* 動作確認go run errors-exercise.go > 1.4142135623746899
> 0 cannot Sqrt negative number: -2 => 負の数が指定された場合は、エラーが返却されている
この先もいくつかありますが、取り急ぎ重要そうなところは大体終わったので、ここまでにしようと思います。
最後に
Goのお勉強をしてみたのですが、エラーの扱いとか、レシーバ周りは少し書き慣れないといけない感じはしますが、それ以外はシンプルな感じがしました。
気が向いたら今回スキップした部分についてもやってみようと思います。
トラブルシューティング
コンパイル環境と実行環境が合ってないよ的なエラー
- エラー内容
compile: version "go1.17.6" does not match go tool version "go1.17.3"
-
原因
goenvにより、GOPATHやGOROOTは`1.17.6`を向いているのに、肝心の`go`コマンドが`1.17.3`を向いている。PATHの設定がおかしいのが原因echo $GOPATH > /Users/xxx/go/1.17.6 echo $GOROOT > /Users/xxx/.goenv/versions/1.17.6 which go > /usr/local/go/bin/go go version > go version go1.17.3 darwin/amd64 echo $PATH > /usr/local/go/bin:/Users/xxx/.goenv/versions/1.17.6/bin:/Users/xxx/.goenv/shims:/Users/xxx/.goenv/bin:省略
- 対処
PATHの先頭に`/usr/local/go/bin`がきているのが原因なので、これを設定してしまってる処理を`.bash_profile`から削除。
自分の場合は昔、GOを触った時に追加したであろう以下の設定を削除することで直りました。- export PATH="$PATH:/usr/local/go/bin"