バックエンドのプログラミング言語として、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
  • GOPATHGOROOTを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を使おうと思います。
(他にGoLandATOMなどもあるようです。)

  • 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/randpackage 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という関数を定義している
    • 引数を取ることができ、変数名の後ろに型を記載する
    • 戻り値にも型を記載する
  • 動作確認
    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))
    }
  • 説明
    • 数値の定数は高精度な値
    • 型のない定数は、状況によって必要な型を取る
    • 定数bigintの最大値を超えるため、intとして振る舞うことはできず、needInt(big)はエラーになる
    • 定数smallintとしても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 初期化: 条件: 後処理 {}の形式
    • 初期化と後処理は省略可能
    • 条件を省略すると無限ループになる
    • breakcontinueもできる
  • 動作確認
    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-ifelseブロックで参照できる
  • 動作確認
    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 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 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)
        }
    }
  • 説明
    • rangefor文で利用するもので、slicemapのindexと要素の二つの変数を返す
    • _へ代入することで、捨てることもできる
  • 動作確認
    go run range.go 
    > 0 taro
    > 1 jiro
    > 2 saburo
    > taro
    > jiro
    > saburo

練習問題: slice

  • 問題
    • 長さdysliceに、各要素が8bitのunsigned int型で長さdxsliceを割り当てたものを返す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)
    }
  • 説明
    • mapsliceを使ってサイズ指定して生成することも、直接初期化することもできる
    • 要素の存在確認方法がやや独特。
  • 動作確認
    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
  • 動作確認
    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()を呼び出すことで無名関数のオブジェクトを受け取り、09までの数値を引数に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が起きる
    • switchi.(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()`みたいなことが簡単にできないのが手間に感じる...
  • 動作確認
    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`かどうかを判定し、エラー処理を行う
  • 動作確認
    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"
Posted in: go