LoginSignup
161
152

More than 5 years have passed since last update.

私的メモ: golang 学習 - A Tour of Go をなぞる

Last updated at Posted at 2014-07-26

(この内容は2014年07月時点のものです)

記事中の A Tour of Go のコードは、
このライセンス のもと、 The Go AuthorsGoogle Inc. に帰属します

参考リソース

概観として

オフィシャル系

更に

概要

  • A Tour of Go をなぞる
  • 写経する
  • すぐ写経するんじゃなくて、
    1. 当該ファイルを作って (tour15.go みたいな名前で)
    2. 右の解説読んでから
    3. 左のサンプルコード読んで
    4. 右の解説読んで
    5. 写経して
    6. go run tour15.go みたいに実行して結果見る
    7. 分からないところがあれば、後述されていることがあるので、飛ばして進む

あんまりのんびりやっても、まとまって頭に入らないので、
出来る限りまず詰め込む。

72 個あったので、1日10個で7日間で1周して、1日休んで3日間で復習して1日休んで残り2日間をバッファにする感じ?

Tour 1 - 10

基本形。シンプルな文字列出力。

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello go!")
}
  • package main, import, func main(){} をとりあえずテンプレとして覚える
  • コメントは // または /* */
  • import ("math/rand")rand.Intn(10) パスの最後がモジュール名として取り込まれる
  • math.pimath.Pi の違い = 先頭が大文字はモジュール外部から参照可能
  • 出力対象文字列をダブルクォートではなくシングルクォートするとエラーになる

関数

  • 引数: 型が変数名の後 に来る
  • 戻り値の型引数のパレンシスの後に書く
  • 引数のカッコと関数名の間にはスペースを空けない
func add(x int, y int) int {
    return x + y
}

2つ以上の関数の引数が同じ型の場合、最後の型を残して 省略することができる

上記の関数は下記のように、引数の型指定を省略できる。

func add(x, y int) int {
    return x + y
}

関数は複数の戻り値を返すことが出来る

func swap(x, y string) (string, string) {
    return y, x
}

代入は := と書く? → 後で。

  • 戻り値には名前を付けて返すことが出来る。省略も出来る。つまり return だけ。
func split(sum int) (x, y int) {
    x = sum - 10
    y = sum - x
    return
}

代入は = とも書ける? := との違いは? → 型と変数宣言の説明の後で。

Tour 11 - 20

var による変数の宣言 (Cみたいに可読性と性能向上のため?)

var i int
var c, python, java bool // 最後が型の宣言になる

var での宣言と同時に初期化も出来る。初期化子が指定されている場合は型を省略可能。

var i, j int = 1, 2
var c, python, java = true, false, "no!"
// fmt.Println(i, j, c, python, java) -> 1 2 true false no!

:= について

Short variable declarations

関数内では、 var 宣言の代わりに、暗黙的な型宣言ができる := の代入文を使うことができます。

なお、関数外でのすべての宣言にはキーワードでの宣言(var, func, など)が必要で、 := での暗黙的な宣言は利用できません。

k := 3var k int = 3 の短縮記法ということかな。

上記引用説明の通り、関数外では短縮記法は出来ず var を使った宣言をしなければ行けない。

組み込み型 - ビルトインタイプ

  • bool
  • string
  • int int8 int16 int32 int64,
    uint uint8 uint16 uint32 uint64 uintptr
  • byte // uint8 の別名
  • rune // int32 の別名, Unicode のコードポイントを表す
  • float32 float64
  • complex64 complex128
package main

import (
    "fmt"
    "math/cmplx"
)

var (
    ToBe bool = false
    MaxInt uint64 = 1<<64 - 1
    z complex128 = cmplx.Sqrt(-5 + 12i)
)

func main () {
    const f = "%T(%v)\n"
    fmt.Printf(f, ToBe, ToBe)
    fmt.Printf(f, MaxInt, MaxInt)
    fmt.Printf(f, z, z)
}
  • 見ての通り const は定数。
    • 宣言に := は使わない
    • 関数の戻り値を利用するなどして定義出来ない (演算結果は大丈夫)
    • 先頭文字のみ大文字が慣習の模様
    • 数値定数は高精度な値で保持される
    • 型指定のない定数は状況に応じて必要な型を取る
  • var (一括) で変数宣言出来るんだ……。
  • そして fmt.Printf() のフォーマットで %T は型を、 %v は値を表すっぽい。

ループ

  • Go には while がない。
  • for だけ。
    for i := 0; i < 10; i++ {
        sum += i
    }
  • 初期化, 条件, ループ毎処理を囲むカッコは不要 (Cライクな文法と比較して)
  • セミコロンで区切る
  • Cライクな文法と同じくそれぞれ省略は可能
  • その際セミコロンも省略可能
    for sum < 1000 {
        sum += sum
    }

無限ループは

for の条件ブロックから条件を省略する。

package main

func main() {
    for {
        // 何かあれば脱出処理
    }
}

go run tour20.go した場合は Ctrl+C で終了。

まとめ: Tour 1 - 20

  • 基本の形
  • 出力
    • fmt.Println("string", "next_str")
    • fmt.Printf("%T, %v", val, val)
  • 変数, 型, 定数
  • 関数 (引数と、戻り値の型・名前)
  • ループ

自分なりにまとめた形↓

package main

import "fmt"

func to_negative(x int) int {
    const Minus int = -1
    return Minus * x
}

func main() {
    sum := 1

    for sum < 500 {
        sum += sum
        fmt.Println(to_negative(sum))
    }
}

疑問 : 名前空間ってどうなってるんだろう?

Tour 21 - 41

If

  • 条件ブロックにカッコがない

ここと関係ないけど -x-1 * x になるのか!

    if x < 0 {
        return sqrt(-x) + "i"
    }
  • for i := 10; i < 100 {} みたいに、条件の前に短い文を書ける
    • その短い文で宣言した変数はその if スコープ (else 含む) のみで有効
    if v := math.Pow(x, n); v < lim {
        return v
    }

ニュートン法で平方根を求める

How to Write Go Code - The Go Programming Language
を参考にさせて頂いた。

自分的なうまく出来てなかったポイントは、

  • カッコを使った計算優先度付
    • z - (z*z - x) / 2 * z ← 間違い
    • z - (z*z - x) / (2 * z) 正しい

Sqrt() の実装は上記ではこのようになっている。

// Package newmath is a trivial example package.
package newmath

// Sqrt returns an approximation to the square root of x.
func Sqrt(x float64) float64 {
    z := 1.0
    for i := 0; i < 1000; i++ {
        z -= (z*z - x) / (2 * z)
    }
    return z
}

ループを回すときの直前に求めたzの値がこれ以上変化しなくなったとき (または、差がとても小さくなったとき) に停止するようにループを変更してみてください。

に対応しているバージョンは、

gotour-answers/sqrt.go at master · guanqun/gotour-answers

を参考にさせて頂くと、


func Sqrt(x float64) float64 {
    last_z, z := x, 1.0

    for math.Abs(z-last_z) >= 1.0e-6 {
        last_z, z = z, z-(z*z-x)/(2*z)
    }

    return z
}

のようになっていた。なるほどです…。

loops.go - go-tour - A Tour of the Go Programming Language - Google Project Hosting

を見ると、

package main

import (
        "fmt"
        "math"
)

const delta = 1e-6

func Sqrt(x float64) float64 {
        z := x
        n := 0.0
        for math.Abs(n-z) > delta {
                n, z = z, z-(z*z-x)/(2*z)
        }
        return z
}

func main() {
        const x = 2
        mine, theirs := Sqrt(x), math.Sqrt(x)
        fmt.Println(mine, theirs, mine-theirs)
}

となっている。

構造体

C 言語と似てる。

type Vertex struct {
    X int
    Y int
}

// という書き方も出来るし、
// 次のような書き方も出来る

type Vertex2 struct {
    X, Y int
}

var (
    p = Vertex{1, 2}  // has type Vertex
    q = &Vertex{1, 2}  // has type *Vertex
    r = Vertex{X: 1}  // Y:0 is implicit
    s = Vertex{} // X:0 and Y:0
)

func main() {
    v := Vertex{1, 2}
    v.X = 4
    fmt.Println(v.X)

    fmt.Println(p, q, r, s)
}
  • 宣言は type 名前 struct {} という感じ。
  • 名前{フィールド1, フィールド2} という感じで値を入れる。

ポインター

  • ポインタはあるがポインタ演算はない
  • q := &p のようにアドレス演算子でポインタを得ることが出来る
  • q.X のようにそのままフィールドにアクセス出来る

new()

var t *T = new(T) または t := new(T) でゼロ初期化して変数を割り当てたメモリのポインタを返す
つまり、

v := new(Vertex)
または

var v *Vertex = new(Vertex)

v := &Vertex{0, 0}

は同じということ?

試したところ出力結果は同じになった。

スライス

  • []T は、 T 型の要素をもつスライスである.
    • 例: p := []int {1, 2, 3}
  • p[i] で中身を参照出来る
  • len(p) で長さを取得できる

スライスのスライス

  • p[0:0], p[3:3] は空
  • p[0:1] は 0 番目の要素だけの配列のコピー
  • p[n:m] は n から m-1 番目までの再スライスとなる (Python とかと同じ)
  • p[:4] は 0-3番目の再スライス (ゼロが省略出来る)
  • インデックスは正数のみ p[-1] のような指定は出来ない
  • スライスは 長さ(len)容量 (cap) を持っている (後述)

make() でのスライス生成

  • a := make([]int, 1, 5)

  • 容量 はその配列を拡大出来る最大値 (それ以上 append 出来ない)

  • ところで、 a := make([]int, 2, 5)a[2] = 10 → エラーになることに注意

これまでに出てきた出力フォーマット子

  • %d: 十進
  • %v: そのままの値 (配列も出力出来る) (value の頭文字?)
  • %T: 型
  • %s: 文字列

nil スライス

次のようなコードがある。

var z []int
fmt.Println(z, len(z), cap(z))

この時、 z == niltrue である
z は 中身なし、長さも容量も 0 である。 → nil

range

slice や map をイテレートしてする。
下記、 i は添字, v は値が格納される。

    for i, v := range []int{1, 2, 3, 4, 5} {
        fmt.Printf("1 + %d = %d\n", i, v)
    } 

では、毎回 添字と値の両方を入れる変数を用意しないとダメかと言えばそうではなく、
上記例で言えば、 i_ にして捨てることが出来る。

添字だけが欲しい場合は、

for i := range []int{1,5,10,15} {
    fmt.Printf("i ==%d\n", i)
}

のようにして得られる。(というか2番めの変数が無いと値が得られない)

ところで tour of go の要所要所で出題される Exercise の答えは

にある模様。
例えば、 tour36 であれば
https://code.google.com/p/go-tour/source/browse/solutions/slices.go

にある (下記コード)。

package main

import "code.google.com/p/go-tour/pic"

func Pic(dx, dy int) [][]uint8 {
        p := make([][]uint8, dy)
        for i := range p {
                p[i] = make([]uint8, dx)
        }

        for y, row := range p {
                for x := range row {
                        row[x] = uint8(x * y)
                }
        }

        return p
}

func main() {
        pic.Show(Pic)
}

上記コードの注意点

  1. p := make([][]uint8, dy) しただけでは、 [[] [] [] [] [] [] [] [] []] のような dy 個の要素をスライスとして持ったスライスが出来るだけ。 各要素を dy 個の要素を持つスライスで初期化してやらなければいけない。
  2. 2重ループで2重配列の各要素にアクセスし、値を入れていく
    • ここで、range が返す値は int なので uint8() して戻り値を揃えることに注意
  3. 上記では描画する式は x * y となっている (tour36 の解説にあるように (x+y)/2 でも x^y でも良い)

map

連想配列、辞書みたいなもの。

type Vertex struct {
    Lat, Long float64
}

var m map[string]Vertex // 変数の宣言

// またはリテラルで↓

var m2 = map[string]Vertex{
    "key1": Vertex {
        10.00000, 5.00000,
    },
    "key2": Vertex {
        2.00000, 4.00000,
    },
}

func main() {
    m = make(map[string]Vertex)
    m["Bell Labs"] = Vertex{
        40.68433, -74.39967,
    }
    fmt.Println(m["Bell Labs"])

    fmt.Println(m2["key1"])
}
  • 上記 "key1": VertexVertex は省略出来る
  • 更新, 挿入: m["keyname"] = elem
  • 参照: elem = m["keyname"]
  • 値の削除: delete(m, "keyname")
  • キーの存在確認 (2つの値を使う): elem, ok = m[key], ok の値が bool で返る。なお、存在しない場合、elem は該当する型のゼロになる。

string パッケージを使って単語を空白で区切る

import "strings" して、

    m := make(map[string]int)
    for _, f := range strings.Fields(s) {
        m[f]++
    }
    return m  

こんな感じにすると単語の頻度が取れる。


ところで疑問: if の and や or はどうするのだろう?

Goプログラミング言語仕様 - golang.jp

を見つけた。結果、

  • and: &&
  • or : ||

C 言語と同じだった。


Tour 42 - 56

関数

関数も変数に入れられる。

    hypot := func(x, y float64) float64 {
        return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(3, 4))

クロージャー

func adder() func(int) int { // (A)
    sum := 0
    return func(x int) int { // (B)
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder() // (C)
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i), // (D)
            neg(-2*i), // (D)
        )
    }
}
  • (A): func(int) int は戻り値の "型" なので引数の変数名は要らない
  • (B): こちらでは具体的な "関数" の実体を返しているので引数の変数名がある
  • (C): それぞれの変数に関数がバインドされた。 sum := 0 が実行され、関数自体が変数に渡される
  • (D): pos(i) は既に
  func(x int) int {
      sum += x
      return sum
  }

と同じ関数になっているので、 sum := 0 は再び処理されてはいない

フィボナッチ数列 (No.44 Exercise)

func fibonacci() func() int {
    f, g := 0, 1
    return func() int {
        f, g = g, f+g // 前の値と新しい値を扱う定形
        return f
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
       // ↑初期条件(a0 = 0, a1 = 1) さえ有れば引数を取ずに数列が生成出来る
    }
}

switch

  • case の最後で自動的に break するようになっている
  • break させずに貫通させたい場合は fallthrough を case の最後に記述する
package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Print("Go runs on ") // (A)
    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
    default:
        // freebsd, openbsd,
        // plan9, windows...
        fmt.Println("%s.", os)
    }
}
  • (A): fmt.Print() 改行を入れずに出力する命令

time パッケージのサンプル

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("When's Saturday?")
    today := time.Now().Weekday()
    switch time.Saturday {
    case today + 0:
        fmt.Println("Today.")
    case today + 1:
        fmt.Println("Tomorrow.")
    case today + 2:
        fmt.Println("In two days.")
    default:
        fmt.Println("Too far away.")
    }

    fmt.Printf("%T, %v\n", time.Saturday, time.Saturday)
    fmt.Printf("%T, %v\n", today, today)
    fmt.Printf("%T, %v\n", today+1, today+1)
    fmt.Printf("%T, %v\n", time.Tuesday+2, time.Tuesday+2)
    fmt.Printf("%T, %v\n", time.Saturday+1, time.Saturday+1)
}

出力結果

When's Saturday?
Tomorrow.
time.Weekday, Saturday
time.Weekday, Friday
time.Weekday, Saturday
time.Weekday, Thursday
time.Weekday, %!v(PANIC=runtime error: index out of range)

time.Sunday は 0 的なものが入っているということだと思う
(time.Weekday の 7 番目はインデックス範囲外で未定義ということ)

条件式の無い switch は 長い "if-then-else" の代わりとして使える

func main() {
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("Good morning!")
    case t.Hour() < 17: // (A)
        fmt.Println("Good afternoon!")
    default:
        fmt.Println("Good evening!")
    }
}
  • (A): 12時未満ならその上の case t.Hour() < 12 で break していることに注意

複素数の立方根

How to take a cube root in Go? - Stack Overflow

tour24 を参考にした。

package main

import (
    "fmt"
    "math/cmplx"
)
const delta = 1e-6

func Cbrt(x complex128) complex128 {
    z := x
    n := 0.0 + 0i  // Point!: 複素数の宣言の仕方。これで complex128 になる
    for cmplx.Abs(n-z) > delta {
        n, z = z, z-(cmplx.Pow(z, 3)-x)/(3*cmplx.Pow(z, 2))
    }
    return z
}

func main() {
    const x = 2
    mine, theirs := Cbrt(x), cmplx.Pow(x, 1.0/3)  // Point!: 1.0/3 で 3√ になる
    fmt.Println(mine, theirs, cmplx.Abs(mine-theirs))
}

出力

(1.2599210498953948+0i) (1.259921049894873+0i) 5.218048215738236e-13

複素数と複素数の距離 (ノルム) はスカラなので複素数にはならない

メソッド (tour50 - )

class ではなく、 struct にメソッドを定義する (class がない)。

  • メソッドレシーバー: func と メソッド名の間に書かれるもの。
    • 下記の (v *Vertex) がレシーバ (これは * が付いているのでポインタレシーバ)。 メソッドを定義する対象の 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())
}

出力

5

型にメソッドを定義する場合

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())
}

疑問

  • (v *Vertex), (f MyFloat)* の有無の違いは何か
    • 後で出てくる。
  • * は対象型へのポインタ型を表す。これを指定した場合のその操作において、
    • データがコピーされない (メモリ効率)
    • 対象データを直接変更可能
    • MyFloat* がついてないのは、単純な型はポインタを介さなくても操作出来るから? (C 言語みたいな言語的な仕様?)
func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

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

の出力が

&{15 20}

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

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

の出力が

&{3 4}

になる。これは、ポインタレシーバーを使わない場合は、メソッド内では値のコピーに対して変更を行い、基の値に影響を与えないから。

インターフェース

  • 型にメソッドを実装 (宣言みたいな感じ) することでインターフェースを実装できる
type Abser interface {
    Abs() float64
}

func main() {
    var a Abser
    f := MyFloat(-math.Sqrt2)
    i := 1
    a = f
    a = i // 実験
}

type MyFloat float64 

func (f MyFloat) Abs() float64 {
    ...
}
  1. aAbser 型として宣言された
  2. fMyFloat 型として初期化された

ここで、 fAbs() メソッドを持っている → Abser のインターフェースを
備えているので、 Abser 型である a には代入可能。

後続行の a = i においては、 Abs() メソッドを定義してない int 型を、
Abser 型である a に代入しようとしているので、下記のようなエラーになる。

./tour53.go:19: cannot use i (type int) as type Abser in assignment:
    int does not implement Abser (missing Abs method)

インターフェースについてもう一つ。

  • インターフェースを実装することを明示的に宣言する必要はない。

http://go-tour-jp.appspot.com/#54 から引用

package main

import (
    "fmt"
    "os"
)

type Writer interface {
    Write(b []byte) (n int, err error)
}

func main() {
    var w Writer

    // os.Stdout implements Writer
    w = os.Stdout

    fmt.Fprintf(w, "hello, writer\n")
}

fmt.Fprintf() の第一引数は io.Writer 型で、io.Writer 型は、
上記で定義したのと同じく Write というインターフェースを備えている。
os.Stdout*os.File という型で、os.Fileへのポインタ。
*os.FileWrite というメソッドを備えているので、 w に代入が可能である (たぶん)。

つまり、 インターフェースが合っていれば 代入操作は許される。

エラー

http://go-tour-jp.appspot.com/#55 から拝借

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)
    }
}

tour56 Exercise:Errors について

Sqrt() の引数が負の場合にエラーを返すようにする実装課題。

Error メソッド中で、 fmt.Print(e) を呼び出すことは、無限ループにプログラムが陥ることでしょう。

については、

fmt.Print(float64(e)) として e を変換することより、これを避けることができます。 なぜでしょうか?

とあり、これは前頁の、

fmt パッケージのさまざまな表示ルーチンは、 error の表示を求められたときにErrorメソッドを呼び出しを自動的に実行します

だと思う。以下を解答例として、

package main

import (
    "fmt"
    "math"
)

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
    fmt.Print(e)
    return fmt.Sprintf("cannot Sqrt negative number: %g", e)
}

const delta = 1e-10

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, ErrNegativeSqrt(f) // (A)
    }

    z := f
    for {
        n := z - (z*z-f)/(2*z)
        if math.Abs(n-z) < delta {
            break
        }
        z = n
    }
    return z, nil
}

func main() {
    fmt.Println(Sqrt(2))
    fmt.Println(Sqrt(-2))
}
  1. (A) において 2 番目の戻り値では ErrNegativeSqrt 型の変数を返しているだけで、その先でも明示的に Error() メソッドを呼び出していない。
  2. fmt パッケージの Print などのメソッドでは上述の通り、 Error() があればそれを呼び出すようになっているので、 Error() の備わっていない float64 などの型に変換することで、無限ループを回避出来た、ということなのだと思う。

http://golang.org/src/pkg/fmt/print.go 見たけど、すぐには該当箇所が分からなかった…。

L:692 v.Error()

の箇所?

Tour 57 - 61

Web servers

http.ListenAndServe("localhost:4000", h)

このメソッドの中では h.ServeHTTP() が呼ばれるようになっているので、
type Hello struct{} に実装したメソッドの内容が処理される。

Exercise: HTTP Handlers

を見ての最初の解答 (間違い)

package main

import (
    "fmt"
    "net/http"
)

type String string

func (str String) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, str)
}

type Struct struct {
    Greeting string
    Punct    string
    Who      string
}

func (str *Struct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, &str.Greeting, &str.Punct, &str.Who) // (A)
}

func main() {
    // (B)
    var hstring String
    var hstruct *Struct
    http.ListenAndServe("localhost:4000", hstring)
    http.ListenAndServe("localhost:4000", hstruct)
}

上記コードの要修正箇所

  • (A): レシーバに *Struct とポインタを指定しているので、構造体変数へのアクセスには & は要らない
    • fmt.Fprint(...)fmt.Fprintf(w, "%s%s %s", str.Greeting, str.Punct, str.Who) と書き直せる
  • (B): ListenAndServe("localhost:4000", foo)fooServeHTTP() を備えていればそれを処理するし、
    下記のように foo の部分に nil を指定していれば、 http.Handle の第 2 引数に備わった ServeHTTP() が処理される

    • http.Handle() の第 1 引数には処理が実行される URL パスを設定する
    http.Handle("/string", String("I'm a frayed knot."))
    http.Handle("/struct", &Struct{"Hello", ":", "Gophers!"})
    err := http.ListenAndServe("localhost:4000", nil)
    

解答例は下記。

http.go - go-tour - A Tour of the Go Programming Language - Google Project Hosting

Images

うーむ…

Rot13 Reader

うーむ…

Tour 62 - 72

並列処理について - Goroutines

go 文のこと。

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

f 、x 、 y、 z の評価は、現在のgoroutineで発生し、 f の実行は、新しいgoroutineで発生します。

実行結果

hello
world
hello
world
hello
world
hello
world
hello

goroutineは、同じアドレス空間で実行されるため、共有メモリへのアクセスは、きちんと同期する必要があります。 sync パッケージは役に立つ方法を提供していますが、他の方法があるので、Goではあまり必要ありません。

チャンネル

データをあっちの世界へ送るためのもの。
送ったデータはあっちの世界から取り出せる。
バッファ に出来る。

ch := make(chan int)

で作成する。

ch <- v    // v をチャネル ch へ送る。
v := <-ch  // ch から受信し、
           // 変数を v へ割り当てる。

なので、

  • <チャンネル> <- <変数> でチャンネルにデータを送る
  • <変数> := <- <チャンネル> でチャンネルからデータを受け取る

http://go-tour-jp.appspot.com/#64 のように、

go sum(a[:len(a)/2], c)

デフォルトでは、片方が準備できるまで送受信はブロックされます。 これは、明確なロックや条件変数がなくても、goroutineの同期を許します。

というわけで、素直に書いても同時に変数を書き換えることがない。

バッファとして初期化

ch := make(chan int, 100)

100 の部分がバッファの長さ。

バッファがいっぱいになる時だけ、バッファchannel への送信をブロックします。 バッファが空の時には、受信をブロックします。

とのこと。

注意: 送り手のチャネルだけを閉じるようにしてください。受け手ではありません。 もし閉じたチャネルへの送信すると、パニック( panic )になるでしょう。

もう一つ注意: チャネルは、ファイルと同じようなものではありません。 通常は、閉じる必要はありません。 閉じるのは、これ以上来る値がないことを受け手が知らせなければならないときにだけ必要です。 例えば、 range ループを終了することなどです。

v, ok := <-ch

と書いて、 ok を調べることでチャンネルがチャンネルからのデータが尽きた or 閉じていることが分かるようになっている。

明示的なチャンネルの閉門(?)は

close(ch)

で行う。

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x        // x が n 回バッファとしてのチャンネルに放り込まれる
        x, y = y, x+y // x が 一つ前の x+y に更新される
    }
    close(c)
}

func main() {
    c := make(chan int, 12)
    go fibonacci(cap(c), c)
    for i := range c { // バッファに入っている数だけプリントされる
        fmt.Println(i)
    }
}

Select

select ステートメントは、goroutineを複数の通信操作で待たせます。

select は、それの case の条件で、いずれかを実行できるようになるまでブロックし、そして、条件が一致した case を実行します。 もし、複数が一致した場合、 case はランダムに選ばれます。

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {  // (C)
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {  // (A)
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)  // (B)
}

実行結果

0
1
1
2
3
5
8
13
21
34
quit
  • (A): 本来であれば c チャンネルからデータを 10 回受信して、 quit チャンネルに 0 を送って終わる
  • (C): 無限ループ内で、 case のいずれか、つまり c <- x<-quit が 実行できるまで select でブロックする。実行可能に慣ればその case 内を実行する。

以下、理解のメモ。自信なし。

  1. (A) go 文があるのでブロックされず (= 後続の命令を待たせない) 、現在の処理ラインとは別の goroutine で実行されるので、すぐに (B) 平行して実行される。
  2. (A) では fmt.Println(<-c) の処理で c がまだ空なのでブロック状態 (データが受信可能になるまで待機) になる。
  3. (A)と平行して処理される (C) の中で、case c <-x が実行可能と判断されて、
  4. c チャンネルには x(=0) のデータが送られる。
  5. c チャンネルのブロックが解けて、 (A) の fmt.Println(<-c) が処理され、 0 が出力される。
  6. (A) のある for ... i++ {}i が1つインクリメントされる
  7. その後、 (C) 内の x, y = y, x+y が処理される。
  8. (C) 内では for{} により無限ループになっているので、再度 case c <-x が実行される
  9. 後続の x, y = y, x+y が処理される。
  10. c チャンネルに再びデータが入ったので (A) の fmt.Println(<-c) が処理される。
  11. 繰り返し
  12. i が 10 になったところで、 quit0 が送られ、
  13. (C) 内の case <-quit が処理され fibonacci() 自体は return され、終了する。

ここで例えば、

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {  // (C)
        select {
        case c <- x:
            x, y = y, x+y
            fmt.Println("----point----")
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

として、出力結果を見ると、

0
----point----
----point----
1
1
----point----
----point----
2
3
----point----
----point----
5
8
----point----
----point----
13
21
----point----
----point----
34
quit

となる。なぜ、先述の (C) のブロックが 2回連続して出力されているか。

(C) の select により、(A) の goroutine が待たされているからではないかと思う。

select は、それの case の条件で、いずれかを実行できるようになるまでブロックし、そして、条件が一致した case を実行します。

順序的な機序は…分からない…ただのタイミング的なもの?

select における default

どの case にも一致しないしない場合は default のブロックが実行されるとのこと。
ブロックしないで送受信する場合は default が使える、とのこと。

バイナリツリー

二分木をトラバースして、同じ順序で値が保持されているかを検査する。
ずっとまえに学校の講義で C でやったのを忘れているので、

2つのバイナリツリーが同じ順序で保持しているかどうかを確認する機能は、多くの他の言語でとても複雑です。

というところで Go の便利さが実感できない。
あとで Python でもやってみて比較したい。

Web クローラー

  • あるURLが取得済みかどうかの履歴を取る (map で)
  • goch := make(chan 何かの型) を使う
  • スケルトン上では再帰をで Crawl の depth を減らしていって、0 になったら脱出してる

までは分かったけど、そこから分からない…

の解答例を見させてもらった。

// Copyright 2012 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build ignore

package main

import (
    "errors"
    "fmt"
    "sync"
)

type Fetcher interface {
    // Fetch returns the body of URL and
    // a slice of URLs found on that page.
    Fetch(url string) (body string, urls []string, err error)
}

// fetched tracks URLs that have been (or are being) fetched.
// The lock must be held while reading from or writing to the map.
// See http://golang.org/ref/spec#Struct_types section on embedded types.
var fetched = struct {
    m map[string]error
    sync.Mutex
}{m: make(map[string]error)}

var loading = errors.New("url load in progress") // sentinel value

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
    fmt.Printf("depth: %v\n", depth)
    if depth <= 0 {
        fmt.Printf("<- Done with %v, depth 0.\n", url)
        return
    }

    fetched.Lock()
    if _, ok := fetched.m[url]; ok {
        fetched.Unlock()
        fmt.Printf("<- Done with %v, already fetched.\n", url)
        return
    }
    // We mark the url to be loading to avoid others reloading it at the same time.
    fetched.m[url] = loading
    fetched.Unlock()

    // We load it concurrently.
    body, urls, err := fetcher.Fetch(url)

    // And update the status in a synced zone.
    fetched.Lock()
    fetched.m[url] = err
    fetched.Unlock()

    if err != nil {
        fmt.Printf("<- Error on %v: %v\n", url, err)
        return
    }
    fmt.Printf("Found: %s %q\n", url, body)
    done := make(chan bool)
    for i, u := range urls {
        fmt.Printf("-> Crawling child %v/%v of %v : %v.\n", i, len(urls), url, u)
        go func(url string) {
            Crawl(url, depth-1, fetcher)
            done <- true
        }(u)
    }
    for i, u := range urls {
        fmt.Printf("<- [%v] %v/%v Waiting for child %v.\n", url, i, len(urls), u)
        <-done
    }
    fmt.Printf("<- Done with %v\n", url)
}

func main() {
    Crawl("http://golang.org/", 4, fetcher)

    fmt.Println("Fetching stats\n--------------")
    for url, err := range fetched.m {
        if err != nil {
            fmt.Printf("%v failed: %v\n", url, err)
        } else {
            fmt.Printf("%v was fetched\n", url)
        }
    }
}

// fakeFetcher is Fetcher that returns canned results.
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
    body string
    urls []string
}

func (f *fakeFetcher) Fetch(url string) (string, []string, error) {
    if res, ok := (*f)[url]; ok {
        return res.body, res.urls, nil
    }
    return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher is a populated fakeFetcher.
var fetcher = &fakeFetcher{
    "http://golang.org/": &fakeResult{
        "The Go Programming Language",
        []string{
            "http://golang.org/pkg/",
            "http://golang.org/cmd/",
        },
    },
    "http://golang.org/pkg/": &fakeResult{
        "Packages",
        []string{
            "http://golang.org/",
            "http://golang.org/cmd/",
            "http://golang.org/pkg/fmt/",
            "http://golang.org/pkg/os/",
        },
    },
    "http://golang.org/pkg/fmt/": &fakeResult{
        "Package fmt",
        []string{
            "http://golang.org/",
            "http://golang.org/pkg/",
        },
    },
    "http://golang.org/pkg/os/": &fakeResult{
        "Package os",
        []string{
            "http://golang.org/",
            "http://golang.org/pkg/",
        },
    },
}
  • Crawl() 自体を並行処理するわけではなく、 Crawl() の中で並行処理する
    • データの取得処理自体は Fetcher の仕事なので、並行にするのはそいつ
  • sync.Mutex とか .Lock(), .Unlock() とか使ってる
    • ロック機構でデータの書き込みのバッティングを避けている
  • fetched.m[url] = loadingfetched.m[url] = err で状態管理してる。 err には fetcher.Fetch(url) のエラー結果が入るので、正常終了の場合は nil が入る
  • それぞれの body をどの for で読みだしているか
    • for ではなく、 (*f)[url] を再帰的に参照していっている
    • depth がデクリメントされていき、0 になると Crawl() の再帰ループから return される。
  • done は何のためにあるのか。削除するとどうなるか。

    • done <- true をコメントアウトすると
      fatal error: all goroutines are asleep - deadlock!
    
      goroutine 16 [chan receive]:
      main.Crawl(0xda590, 0x12, 0x4, 0x2208190318, 0x14aef8)
          /golang/tour71.go:73 +0xb9d
      main.main()
          /golang/tour71.go:79 +0x78
    
      goroutine 19 [finalizer wait]:
      runtime.park(0x162c0, 0x14ae50, 0x14a209)
          /usr/local/go/src/pkg/runtime/proc.c:1369 +0x89
      runtime.parkunlock(0x14ae50, 0x14a209)
          /usr/local/go/src/pkg/runtime/proc.c:1385 +0x3b
      runfinq()
          /usr/local/go/src/pkg/runtime/mgc0.c:2644 +0xcf
      runtime.goexit()
          /usr/local/go/src/pkg/runtime/proc.c:1445
    
      goroutine 20 [chan receive]:
      main.Crawl(0xda610, 0x16, 0x3, 0x2208190318, 0x14aef8)
          /golang/tour71.go:73 +0xb9d
      main.func·001(0xda610, 0x16)
          /golang/tour71.go:67 +0x50
      created by main.Crawl
          /golang/tour71.go:69 +0x977
    
      goroutine 24 [chan receive]:
      main.Crawl(0xe0050, 0x1a, 0x2, 0x2208190318, 0x14aef8)
          /golang/tour71.go:73 +0xb9d
      main.func·001(0xe0050, 0x1a)
          /golang/tour71.go:67 +0x50
      created by main.Crawl
          /golang/tour71.go:69 +0x977
    
      goroutine 25 [chan receive]:
      main.Crawl(0xe0090, 0x19, 0x2, 0x2208190318, 0x14aef8)
          /golang/tour71.go:73 +0xb9d
      main.func·001(0xe0090, 0x19)
          /golang/tour71.go:67 +0x50
      created by main.Crawl
          /golang/tour71.go:69 +0x977
      exit status 2
    

    done チャンネルにデータが入らないと <-done がずっと待機になるので終了した感じかな…
    - <-done をコメントアウトすると、

      depth: 4
      Found: http://golang.org/ "The Go Programming Language"
      -> Crawling child 0/2 of http://golang.org/ : http://golang.org/pkg/.
      -> Crawling child 1/2 of http://golang.org/ : http://golang.org/cmd/.
      <- [http://golang.org/] 0/2 Waiting for child http://golang.org/pkg/.
      <- [http://golang.org/] 1/2 Waiting for child http://golang.org/cmd/.
      <- Done with http://golang.org/
      Fetching stats
      --------------
      http://golang.org/ was fetched
    

    で終わった (データが全てトラバースされてないまま終わった)。
    done チャンネルが同期の役割をしていて、<-done のあるブロックは go を掛けたブロックの終了を待機している状態になっている。

(なんか C 言語の fork を学んでいた時に似ている…難しい…)

func main() {
    buf := make(chan int, 5)  // (A) バッファとして使う
    buf <- 1
    buf <- 2
    buf <- 4
    buf <- 8
    close(buf)  // (B) これがないと↓のrangeループの終了が出来ない
    for v := range buf {
        fmt.Println(v)
    }
}
  • (A) バッファとして使う場合は、 make() の第2引数の大きさ指定が無いとダメ
  • (B): 「もうチャンネルには送信しないよ」という close(ch) がないと、 range ループがちゃんと終了しない
  • (A), (B) それぞれ、または両方コメントアウトしてみると下記エラーが出る
  fatal error: all goroutines are asleep - deadlock!

実践Go言語 - golang.jp によると、

マップと同じくチャネルは参照型であり、makeを使って割り当てられます。作成時にオプションの整数パラメータを指定すると、チャネルのバッファサイズとして使われます。この値のデフォルトはゼロであり、ゼロのときバッファリングは行われず同期チャネルとなります。

今回はWebクローラーのプログラムでは make() にサイズ指定がないので、同期チャネルとしての利用となる。

シンプルなチャネル利用のイディオム例 (実践Go言語 - golang.jp から)

go list.Sort()  // list.Sortを並列実行する(処理の完了は待たない)

チャネルを利用した優れたイディオムはたくさんありますが、手始めにひとつ紹介します。前のセクションではソート処理をバックグラウンドで起動しましたが、これにチャネルを使って起動したゴルーチンのソート完了を待つよう変更できます。

c := make(chan int)  // チャネルの割り当て
// ゴルーチンとしてsortを起動。完了時にチャネルへ通知
go func() {
    list.Sort()
    c <- 1  // 通知を送信。値は何でも良い
}()
doSomethingForAWhile()
<-c   // sortの完了待ち。送られてきた値は破棄

受信側では常に、受信可能なデータが来るまでブロックされます。送信側はチャネルがバッファリングしていないときは、受信側が値を受信するまでブロックされます。チャネルがバッファリングしているときは、送信側がブロックされるのは値がバッファへコピーされる間だけです。このためバッファがいっぱいのときは、受信側で値を取り出すまで待機します。

tour64 より

デフォルトでは、片方が準備できるまで送受信はブロックされます。 これは、明確なロックや条件変数がなくても、goroutineの同期を許します。

tour65 より

バッファがいっぱいになる時だけ、バッファchannel への送信をブロックします。 バッファが空の時には、受信をブロックします。

とりあえず下記を読んで理解しようと思う。

ひとまず以上。これからアプリを書きつつ復習していきたい。
はー。

161
152
0

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
161
152