GopherデビューするのでA Tour of Goを一通りやった – 空の箱

転職先でGoをやるって聞いてたので有給消化中に『初めてのGo言語』を読んでいた。

初めてのGo言語 第2版 ―他言語プログラマーのためのイディオマティックGo実践ガイド

けどそれよりも先にA Tour of Goをやっとくのが正解だった*1。書籍自体はGo言語の仕様について詳しく書かれていてめっちゃいい本なんだけど、100%僕の頭と書籍の使い方が悪かった。

go-tour-jp.appspot.com

「Goの基本完全に理解した」状態で『初めてのGo言語』はもう一回読むことにする。それはそれでより理解が深まりそう。

左にチュートリアル、右にコーディングエディタがあるUIになっている。トピックスごとにベースのコードを用意してくれていて、ガチャガチャ手元で書き換えて、フォーマットして、実行して結果を見るってことができる。途中練習問題も挟んでくれたりする。

A Tour of Goに限らず、僕自身が疑問に思って調べてたこととか試してたことも含まれる。

Naked Return は使うな

boldlygo.tech

一見便利やーんと思うけど、長くなって最後にreturnだけされると戻り値が多かったりすると常に脳内メモリの保持してる値を記憶しないといけないから辛いのか。

Goのswitch

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Print("Go runs on ")
    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
    default:
        
        
        fmt.Printf("%s.\n", os)
    }
}

go fmtでコード整形してもこれ。Kotlinとかの世界からくると若干違和感あって、caseのインデントが下がって欲しい気がしなくもない。Javaとかにあるフォールスルーがないのはめっちゃ快適。

Defer, Panic, and Recover

  1. deferに渡された関数の引数はステートメントが評価されるタイミングのものに決まる
  2. DeferLIFOで実行される
  3. 戻り関数の名前付き戻り値を読み取り、割り当てることができる。

go.dev

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

このときの出力は以下の通り。

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

Goにおけるポインタ

Goにおける *(ポインタ)は、「この関数は呼び出し元の値を変更するつもりがある」という意図を明示的に表現する仕組み と理解。

type User struct {
    Name string
}


func renameValue(u User) {
    u.Name = "Bob"
}


func renamePointer(u *User) {
    u.Name = "Bob"
}

func main() {
    u := User{Name: "Alice"}

    renameValue(u)
    fmt.Println(u.Name) 

    renamePointer(&u)
    fmt.Println(u.Name) 
}

Go’s Declaration Syntax

go.dev

考えたこともなかった。確かに自分が読みやすいと感じるKotlinもTypeScriptも右に型を書いてるなとなった。

Pointers to structs

package main

import "fmt"

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
    p := &v
    fmt.Println(v)
    p.X = 1e9
    fmt.Println(v)
}
  • &を付けることでpvの参照を得ることができる
  • そうしたときに(*p).Xとか書くのはアホらしいのでGoでは普通にp.Xとかけるよ

ということが確認できる。以下出力。

{1 2}
{1000000000 2}
package main

import "fmt"

func main() {
    var primes []int = [6]int{2, 3, 5, 7, 11, 13} 

    var s []int = primes[1:4]
    fmt.Println(s)
}

スライスは配列への参照なので、スライスの要素を書き換えると元の配列の要素も書き変わる

make, append – Sliceのcapを超えた時

makeは無からSliceを作る。

package main

import "fmt"

func main() {
    a := make([]int, 5)
    printSlice("a", a)

    b := make([]int, 0, 5)
    printSlice("b", b)

    c := b[:2]
    printSlice("c", c)

    d := c[2:5]
    printSlice("d", d)
 
    d = append(d, 1)
    printSlice("d", d)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}
  • cap の場合 → 倍にする
  • cap >= 1024 の場合 → 約 1.25倍ずつ増やす
  • つまりappend「新しい配列にコピーしてcapを増やす」という動作もしている
a len=5 cap=5 [0 0 0 0 0]
b len=0 cap=5 []
c len=2 cap=5 [0 0]
d len=3 cap=3 [0 0 0]
d len=4 cap=6 [0 0 0 1]

range – データ構造をiterateする

package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
    for i, v := range pow {
        fmt.Printf("2**%d = %d\n", i, v)
    }
}

mapの操作

package main

import "fmt"

func main() {
    m := make(map[string]int)

    m["Answer"] = 42
    fmt.Println("The value:", m["Answer"])

    m["Answer"] = 48
    fmt.Println("The value:", m["Answer"])

    delete(m, "Answer")
    fmt.Println("The value:", m["Answer"])

    v, ok := m["Answer"]
    fmt.Println("The value:", v, "Present?", ok)
}

goにクラスはない/methodを作るにはstruct+レシーバー引数を作る

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
}

GoのMethods continued vs Kotlinの拡張関数

Goのメソッドは、structで定義した型以外の型に対しても定義できる。*2

package main

import (
    "fmt"
    "math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f 0 {
        return float64(-f)
    }
    return float64(f)
}

func main() {
    f := MyFloat(-math.Sqrt2)
    fmt.Println(f.Abs())
}

GoのMethods continuedはメソッドは元の型に「属する」形で定義されるが、Kotlinの拡張関数は元の型に属さず定義されるあくまで拡張関数。なので、Kotlinの拡張関数はオーバーライドされない。

変数レシーバーとポインタレシーバー

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    v.Scale(10)
    fmt.Println(v.Abs())
}
  • ポインタレシーバーであれば、レシーバ自体が持つ値を変更できる
  • 変数レシーバーはできない

つまりポインタレシーバーで定義されたメソッドはレシーバーに対して副作用があるかもしれないが、変数レシーバで定義されたメソッドはレシーバーに対して副作用がないということになる。

またポインタレシーバーが巨大な場合に変数レシーバーを多用すると毎度変数コピーのオーバーヘッドが大きくなる。

一般的には変数レシーバ または ポインタレシーバのどちらかですべてのメソッドを与え、混在させるべきではない

インターフェースの実装時に破綻するから。

type Printer interface {
    Print()
}

type Incrementer interface {
    Increment()
}

type Counter struct {
    n int
}

func (c Counter) Print() { fmt.Println(c.n) }     
func (c *Counter) Increment() { c.n++ }            

func main() {
    var p Printer = Counter{} 
    var i Incrementer = Counter{} 
}

iに代入しようとしても、Counterは値のためfunc (c *Counter) Increment() { c.n++ }を満たさず、代入できない。

package main

import "fmt"

func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s)

    s, ok := i.(string)
    fmt.Println(s, ok)

    f, ok := i.(float64)
    fmt.Println(f, ok)

    f = i.(float64) 
    fmt.Println(f)
}

switchによる分岐もできる。

package main

import "fmt"

func do(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Twice %v is %v\n", v, v*2)
    case string:
        fmt.Printf("%q is %v bytes long\n", v, len(v))
    default:
        fmt.Printf("I don't know about type %T!\n", v)
    }
}

func main() {
    do(21)
    do("hello")
    do(true)
}

goで代数的データ型は表現できる?

直和型(enum)がないから無理。よってdefaultは実質すべてpanic扱い。

package main

import "fmt"


type Expr interface{}


type Add struct {
    Left, Right Expr
}


type Number struct {
    Value int
}


func Eval(e Expr) int {
    switch v := e.(type) {
    case Number:
        return v.Value
    case Add:
        return Eval(v.Left) + Eval(v.Right)
    default:
        panic(fmt.Sprintf("unknown expression: %T", v))
    }
}

func main() {
    
    expr := Add{
        Left: Number{1},
        Right: Add{
            Left:  Number{2},
            Right: Number{3},
        },
    }

    
    result := Eval(expr)
    fmt.Println(result) 
}

なんでGoにはenumがないんだろう?

github.com

このissueがそう。

  • Goのデフォルト値の問題
  • 外部入力時の処理

ここがハードルになっているっぽい。たしかに未定義のenumを何として扱うか?は悩ましいかも。けどnilでいいような気もするが…🤔

type error interface {
    Error() string
}

事前定義されたこいつを使って独自エラーを定義する。

package main

import (
    "fmt"
    "time"
)

type MyError struct {
    When time.Time
    What string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("at %v, %s",
        e.When, e.What)
}

func run() error {
    return &MyError{
        time.Now(),
        "it didn't work",
    }
}

func main() {
    if err := run(); err != nil {
        fmt.Println(err)
    }
}

Goroutine

最初の一歩

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

Channels

  • チャネルへ値を送る
  • チャネルから受信した変数を代入
  • チャネルを作る

チャネルは

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    fmt.Println("this is sum = ", sum)
    c // send sum to c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    fmt.Println("A")
    go sum(s[len(s)/2:], c)
    fmt.Println("B")
    x, y := // receive from c
    fmt.Println("C")

    fmt.Println(x, y, x+y)
}

何回か実行した出力。非同期に処理されたのがわかる。

# 1
A
B
this is sum =  -5
this is sum =  17
C
-5 17 12

#2
A
B
this is sum =  17
this is sum =  -5
C
17 -5 12

#3
A
this is sum =  17
B
this is sum =  -5
C
17 -5 12

Buffered Channels

バッファ付きのチャネル。

package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch 1
    ch 2
    ch 3 
    fmt.Println(


結果

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /tmp/sandbox2970589188/prog.go:9 +0x58

Range and Close

  • close(ch)でチャネルを閉じる
  • v, ok := でokfalseならチャネルは閉じている
package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    v, ok :=  c
    fmt.Println(v, ok)
    for i := range c {
        v2, ok2 :=  c
        fmt.Println(v2, ok2)
        fmt.Println(i)
    }
    v3, ok3 :=  c
    fmt.Println(v3, ok3)
}

出力。おそらくcloseが非同期で走ったタイミングからfalseに変わっている。

0 true
1 true
1
3 true
2
8 true
5
21 true
13
0 false
34
0 false

Select

準備ができたcaseのうち実行できるものを実行する。caseが複数あればランダムに決まる。掲載されているサンプルコードで動きはわかったけど、これだと絶対最後にcase でreturnとしゴルーチンが終わるので、あんまりいいサンプルじゃない。

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c case "quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i 10; i++ {
            fmt.Println(0
    }()
    fibonacci(c, quit)
}
  • 文法や考え方がめっちゃシンプルで覚えやすい
  • 基本文法は1日あればひとさらいできた(気がする)
    • “A Tour of Go”が教材として優秀説はある
    • あとよくわからんところとか疑問点は都度ChatGPTに教えてもらってた
  • interfaceが他言語と考え方が違って少し戸惑った。特にポインタレシーバーあたり
  • 文法や考え方がめっちゃシンプルなのがわかったので、今の状態で『初めてのGo言語』や三井さん(id:todays_mitsui)にもらった『効率的なGo』を読むとめっちゃ面白く読めそう
  • いろんな言語の勉強を継続的にしてるとプログラミングに必要な要素が大体わかるのでキャッチアップにかかる時間が年々短くなっていってる気がする*3

たぶん普通にソースコードは読めるようになったと思うので、強化学習とお作法の習得のためにechoを解析してみる。

github.com

ついでにIDE(GoLand)にも慣れていく。


元の記事を確認する

関連記事