Go Kyoto(Go勉強会 そうだ京都、行こう) の資料で ひとりハンズオン

A Tour of Goをひとまず終えて、どこから手をつけようか迷っていたところ、次の資料を見つけました。

これは @Jxck さんが京都で行ったハンズ・オンの資料を gist で公開してくれているもの。

Jxckさんのブログはとても参考になるので、そんな人のハンズ・オンが資料だけとはいえ追体験できるならもってこいです。 ということで、この資料に則って、ひとりで擬似ハンズ・オンを始めることにしました。

こうやってハンズオンや勉強会の資料を使い、少しずつでも凄い先達のエッセンスに触れていくのが、今の自分には合っている気がする。 ありがたいことに、資料を公開してくれている人はたくさんいる。 ただ資料として読むのではなく、イベントに参加したつもりで、ひとつひとつ噛み砕いてみます。

前提

ハンズ・オンの前提として、以下が掲げられている。

  • 多言語で基本的なプログラミング知識(Web 系?) がある
  • Go のインストール、開発環境(エディタとか)の準備済み
  • http://go-tour-jp.appspot.com/ を一通り(24, 48, 60, 69, 70 は飛ばしてよし)

なんて自分にピッタリなんだろう。

ツアーは複素数の課題以外飛ばさなかったけど、結局、何が Go らしいコードなのかはつかめなかったし。

タスク管理ツール

ハンズオンの素材には、模擬的なタスク管理ツール作りを用いるようだ。 多分タスク管理が選ばれたことに大きな意味はなく、ツアーを終えただけで実質事始めな参加者向けに、あれこれの構造をイメージしやすいもの、というだけだろう。

標準モジュール且つ main ファイルのみということなので、コマンドラインで使うツールになりそう。 Go でのコマンドライン周りは未体験なので、これも丁度いい。

main

package main と、func main のお話。 ツアー等で触れているので、とくに引っかかる部分はなし。

gofmt による自動整形は、確かに開発環境で自動的にキックされるべきだと思う。 エディタや IDE をきちんと突き詰めずに来たので、その辺と絡めて模索していこう。

Document

知らなんだ。

godoc パッケージ名

で、標準パッケージのドキュメントが読める。 多分パッケージファイルの doccomment を見せていて、-src オプションでソースも読める。 godoc は、自前のコードのドキュメント作成にも標準で使えるように用意されていそう。

http の方は、自分の環境の問題か、起動できなかった。 ちょっと面倒なネットワーク設定の環境を使っているので、他の環境でも発生するようであれば、掘り下げてみよう。

ただ、パッケージ内のエクスポートされている要素をダイレクトに引けるわけではないし、自分の半端な英語力で誤解するのはまだ怖いので、当面は golang.jp の翻訳版をまず見よう。

Print Debug

一番簡便な print debug と、標準のログ出力。 そして init()。

fmt パッケージでの print debug はこれまでも用いてきたけど、フォーマットのことは曖昧にしてきた。 ここも改めてしっかりやらないと。

log パッケージは初。 ロギング用のパッケージで、標準ログへの出力も簡単。 panic とか fatal とかを扱うのもあるが、まだ使用のイメージが掴めない。 資料のフラグ以外にも、フォーマットにはミリ秒などもある。

init() も、これまで使っていない。 初期化処理のための特別な関数だというのは分かるけれど、どう書くべきか。 ドキュメントを見ておく。

以下がポイントっぽい。

  • init() から実行された goroutine は init() 完了まで動かない
  • init() は import と var の評価後に実行される
  • 手続き的な初期化処理に用いる

疑問点。

解説には、

すなわち、初期化処理中に実行されるスレッドは常にひとつだけです。

とあるが、スレッドと goroutine は同一ではなかったんじゃないだろか。 ここで"スレッド"として言わんとするのが、goroutine も含めた並列・並行処理全てであり、init() が複数同時に実行されることはないよう保証されている、ということなら理解できる。 しかし Go でのスレッドやプロセスや goroutine の扱いがまだ曖昧なので、確信は持てない。

それから init() が複数あった場合の扱い。 Playground で複数の init() を書けば、問題なくそれぞれが実行される。 この場合、ファイル内での登場する順序に基いた実行順が保証されるんだろうか。 あるいはひとつの init() に寄せられたりするんだろうか。

そして、"init" という語の扱い。 予約語だろうけど、実際の環境でどの程度制約がかけられているのか。

この辺りは、適当にドキュメントを読んでも見つけきれなかった。

ここいらは init() の基本ぽいので、確認してみる。

まず、実行順序。

package main

import (
    "fmt"
)

var foo int = 1

func init() {
    foo = 3
}

func init() {
    foo = 4
}

func main() {
    fmt.Println(foo)
}

func init() {
    foo = 2
}

Playground では、複数のinit() は常に記述順で実行されている。 言語仕様として確認できたわけではないけれど、合理的だとは思うので、ひとまず記述順での実行を期待しても問題なさそう。

また、init() はファイルのどこに記述しても構わないことも確認できた。 もちろん適切な位置は、実行順と一致する var の後だろう。

次に、複数の init() がひとつに寄せられたりしないのかの確認。

package main

import (
    "fmt"
)

func init() {
    foo := 1
}

func init() {
    foo = 2
}

func main() {
}

2つめの init() で、きちんと foo は undefined。 まとめられてしまうこともない。

最後に語としての"init"の扱い。

まず初期化時の var での扱い。

var (
    init int
)

とすると、次のエラーが起こる。

cannot declare init - must be func

init という名前は、関数であることが必須。 これは当然。

では main や他の関数では。

func main() {
    init := 1
    fmt.Println(init)
}

func foo () int {
    init := 1
    return init
}

どちらも問題ない。 ということは、初期化 var で確認できた制限は、パッケージグローバルな init にのみ課せられているっぽい。 関数のスコープであれば、予約語として弾かれることもない。

独自に init という名を使いたくなるシーンが、Go でどの程度起きるんだろか。 もし起きたとしても、パッケージ初期化の init との混同が起きそうで気持ち悪い。 掘り下げは課題としておき、とりあえず自分では init という名前の使用は避けておこう。

ハンズオンから外れすぎたので、戻る。

Variables

変数宣言の書式や、関数内でのみ可能な := による暗黙的型宣言は、理解に問題なし。

Struct

構造体。(オブジェクト的なもの) メンバは、大文字が Public、小文字が Private

分かりやすいように"オブジェクト"としているんだろうけど、他の言語のオブジェクトとは違いが大きい。 大文字で public となっているけど、"先頭"の文字を入れ忘れたのか、それとも他の仕様がある? この辺は多分、ハンズオンの現場でフォローされてるんだろうなあ。 ひとりハンズオンは Twitter でもしながらやるべきなんだろうか。

ゼロ値は、以前の疑問にやっと答えが出た。 空での初期化時に暗黙的に割り当てられる値で、型ごとに決まっている。

  • bool : false
  • int : 0
  • float : 0.0
  • string : ""
  • pointer : nil
  • function : nil
  • interface : nil
  • slices : nil
  • channel : nil
  • map : nil

すっきりした。

ところでこれ、Struct となってるけど、Type としての話が混ざってるような。 Task{} と &Task{} の違いは、struct というより、宣言した Type を扱うことについてじゃないんだろうか、と思ったけれど、ツアーでも解説でも、struct は Type とセットで扱われている。

つまり、Type が任意の基本型も扱えるものだから勘違いをしていた。 struct は Type で任意のフィールドを持った構造体を扱うためのものであり、基本型ではない。 ツアーにあった以下の記述に引っ張られすぎたみたい。

Goには、クラス( class )のしくみはありませんが、struct型にメソッド( method )を定義できます。

であれば、資料で Task{} と &Task{} について述べている部分は、任意の宣言した Type の話であって、理解できた。 しかしこのポインタ周りはまだ曖昧さが残る。

func main() {
    task := Task{
        Id:     1,
        Detail: "buy the milk",
        Done:   false,
    }

    print(task)
}

これを実行した場合、task の print() で "illegal types for operand: print" というエラーが発生する。 原因は task が print() 可能な型ではなく、Task であること。 これをポインタで代入すると、当然 print() の結果は、"0xfeee1f70" といった、メモリの番地になる。

ここまでは分かる。 さて、任意の Type からポインタで初期化する理由ってなんなんだろうか。 今のところ、Go ではポインタは演算できなくて、参照の制御に用いると理解している。 今回の場合、Task のインスタンス(という表現で良い?)を生成する際に、ポインタで渡さないと、値渡しになって Task の重複でも起こるんだろうか。 あるいは、メソッドの組み立て方に関わってくるんだろうか。

何にしても、今はハンズオンを進めないと、きりがない。

Function

パッケージ内で何かを New するなら"NewXXX"という関数名、となっている。 でも以前見た解説には、あるパッケージで単一の型しか返さないのであれば、冗長な表現を避けましょう、なんてのがあった。 あれは単一という縛りがあったから成立するだけで、やはり汎用的には、New だけでは難しいんだろうなあ。

Method

んー、またもこの記述。

struct にはメソッドが定義できる

struct にしか定義できないとは書いていないけど、任意の Type ではなく struct となっているのは、やはり何か勘違いしてるんだろうか。

ここでのポインタの使い方はわかりやすい。 レシーバを実体にすると値渡しとなり、元の構造体を変更できない。 ポインタレシーバであれば変更できる。

しかし、「引数なし,戻り値あり」なら値渡し、「引数,戻り値無し」なら参照渡しという風に、割り切っちゃっていいんだろうか。

課題1

Task.Detail を書き換えるメソッドの実装。 最初の課題だからかとても簡単。

func (task *Task) Edit(detail string) {
    task.Detail = detail
}

IF

条件部の括弧なしは、ツアー等で確認済み。

そして、三項演算子ないのか…。 まあ、それが Go らしいんだろうなあ。

Slice

slice周りで何か色々勘違いしていたのに気が付かせてもらえた。

  • Go の配列は固定長
  • slice はこの配列を参照する
  • slice には append や copy がある
  • append は元の slice に追加した新たな slice を返す

とても詳しいプレゼンがあるので、次の機会にじっくり。

For

range によるイテレーションは、もっとしっかり仕様を見ないと。

発見は、range がくれる2番めの値だけ欲しい時などに使う"_" について。 これまできちんと動きは見てなかった。

package main

import "fmt"

var _ int

func main() {
    _ = "foo"
    fmt.Println(_)
}

こうすると、fmt.Println(_) で "cannot use _ as value" エラー。 つまり var で型の指定はできるし、任意のタイミングで _ への代入は可能だけれど、それを使おうとした時点でエラーとなる。

アンダースコア以外に、"cannot use" となるシンボルがあるのかとか、他に性質が無いかは後で確認しよう。

課題2

Task をまとめた Tasks 型と、その追加メソッドを実装してみる課題。 課題がシンプルなので、答えもコンパクト。

func NewTasks(owner string) *Tasks {
    tasks := &Tasks{
        Owner: owner,
        Tasks: make([]*Task, 0),
    }
    return tasks
}

func (tasks *Tasks) Add(task *Task) {
    tasks.Tasks = append(tasks.Tasks, task)
}

HTTP

予想と違って、タスクはWeb公開にも対応させるものだった。

net/http については、基本的にツアーで学んだ通り。

Writer インターフェースによって、fmt.Fprintf() でレスポンスを書き込める、という辺りは Go の重要な特徴だと思うのだけど、まだまだきちんとは理解できていない。 ここもどこかで重点的に学ばないと。

handler() の第2引数に渡される *http.Request は、一般的なWebアプリケーションフレームワークのリクエストオブジェクトに相当。

課題3

これまでのコードを net/http でブラウザに出力する課題。

ResponseWriter が Writer インターフェースを満たしているので、fmt.Fprintf での出力がそのままレスポンスに使える。

ヒアドキュメントが `` で済むのは、シンプルでいい。

func TasksHandler(w http.ResponseWriter, r *http.Request) {
    tasks := NewTasks("Gopher")
    tasks.Add(NewTask(1, "buy the milk"))
    tasks.Add(NewTask(2, "eat the pie"))
    tasks.Add(NewTask(3, "go to the bank"))

    // heredoc
    html := `
<!DOCTYPE html>
<title>tasks</title>
<h1>%s's tasks</h1>
`

    fmt.Fprintf(w, html, tasks.Owner)
    fmt.Fprintf(w, "<ul>")
    for _, task := range tasks.Tasks {
        fmt.Fprintf(w, "<li>%s</li>", task)
    }
    fmt.Fprintf(w, "</ul>")
}

func main() {
    http.HandleFunc("/", IndexHandler)
    http.HandleFunc("/tasks", TasksHandler)
    http.ListenAndServe(":3000", nil)
}

Template

Go には HTMLテンプレートも標準パッケージに用意されていた。

テンプレートの書式は {{ }} なので、jinja に慣れてる分見やすい。 テンプレートの実行というか組み立てと出力は Execute(w, tasks)。 テンプレート内では、{{ . }} というように、. で渡した要素を表現する。

エラーが複数戻り値で返るのはいいんだけど、何番目の値かは、せめて標準パッケージでは揃ってるんだろうか。

Closure

TasksHandler() が直接 http.Handler を実装していると、http.HandleFunc() で 値を渡せない。 そこで、TasksHandler() 自体は通常の関数とし、http.Handler を実装した関数を返すようにすることで、結合を緩める。

ところで、クロージャってツアーの際に確認した限り、エンクロージャで無名関数をラップして、個々の関数が値を保持し続ける性質だと理解していた。 でもここで返しているのは単なる無名関数であり、それを戻り値として扱えるのは、Go の関数がファーストクラスオブジェクトだからというのが理由で、クロージャは関係ないような…。

自分が理解しているクロージャという概念がプリミティブ過ぎるのかも知れない。 これも掘り下げないと。

JSON

データの json へのパースは、encoding/json パッケージでシンプルにできるが、戻り値はバイト列なので string(j) が必要な場合もある、と。 でも、なぜ戻り値はバイト列なんだろう。 生のレスポンスとして扱う場合はバイト列の方が流しやすいのかな。

対応する key 名を変更したい場合はタグで指定する。

この箇所は、Go の struct に定義した field 名とは異なるキーを json で使いたい場合なんだろうけど、「タグ」というのをきちんと理解していなかった。 Google App Engine でモデル定義に使ってみたりはしたけど、曖昧なままだったので、仕様を見てみる。

要点はこんな感じ。

  • field にオプションの属性を与える
  • field名 型 タグ という書式
  • タグはその型の全フィールドで共有される
  • リフレクションインターフェースからしか参照できない

タグを指定する文字列リテラルには、参考にしたソースによって、"" と `` を使っているものがあった。 これはタグに " を含むかどうかで使い分けるのかな。 jsonxml では

  Foo string `xml:"foo"`

という風に、タグの目的を添えて値を""で括っているので、通常 `` を使うとしても問題ないのかもしれない。

課題4

ハンズオン最後の課題。

func TasksJSONHandler(tasks *Tasks) func(w http.ResponseWriter, r *http.Request) {
    ret, err := json.MarshalIndent(tasks, "", "    ")
    if err != nil {
        log.Fatal(err)
    }
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, string(ret))
    }
}

Handler を関数返す形にしとくと、こういう処理がぐっと書きやすいのが実感出来ました。

おまけ

slice の扱いは、一見 python の list っぽいけど、大分違いそう。 そういった点を解説してくれているスライドもあったので、そっちを見ながら確認してみよう。

終わり

module 分けと、テストもそれぞれ他の資料を探して、まずはひとり勉強会追体験をしてみる。

この手のイベントの資料を使うと、仕様を読んでもなかなか辿りつけない Go のニュアンスのようなものが得やすい。 Webの解説記事よりも、もっとターゲットやペースがはっきりしているからか、進めやすくもある。

次ももうひとつぐらい、総論っぽい資料を元にひとり勉強会追体験しよう。

ゼロから学ぶGo言語プログラミング(16) A Tour of Go 71~72

f:id:belbo:20140616112930j:plain

A Tour of Goの続き。 ツアーもこれで最後。

71.Exercise: Web Crawler

最後の課題らしく、コードは多め、解説は少なめ。

まず、初期のコード。 今までのサンプルコードよりボリュームがあるので、まずはしっかり読んでみる。

package main

import (
    "fmt"
)

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

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
    if depth <= 0 {
        return
    }
    body, urls, err := fetcher.Fetch(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("found: %s %q\n", url, body)
    for _, u := range urls {
        Crawl(u, depth-1, fetcher)
    }
    return
}

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

// 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/",
        },
    },
}

課題はシンプルなクローラの改良です。

まずmain()は以下の関数実行のみ。

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

実行されるCrawl()。

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
    if depth <= 0 {
        return
    }
    body, urls, err := fetcher.Fetch(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("found: %s %q\n", url, body)
    for _, u := range urls {
        Crawl(u, depth-1, fetcher)
    }
    return
}

クロールの開始点となるURL、そこから何階層辿るかの指定depth、そしてFetcher型を受け取ります。

階層depthが0以下ならそこで終了。 そうでなければ、Fetcherのインターフェースに定義されているFetch()を、引数のURLで実行。 複数の戻り値をbody, urls, errで受けています。 これは実際のクローラーであれば、レスポンスのbodyと、headやbodyに含まれていたURLを切り出したslice、そしてステータスコードなどに基づくエラーを返すんでしょう。 エラーがnilでなければ、標準出力にエラーを記録して終了。 実際にはログでしょうか。

Fetch()が問題なければ、取得したURLとbodyを出力し、URLの詰まったsliceであるurlsをforとrangeで回す。 ここでdepthをひとつ減らしてCrawl()を再帰。 Crawl()はここまでです。

次に、Fetcherインターフェースの実装部分。

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

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

実装であるfakeFetcher型は、stringがキーで値がfakeResult型のポインタになったmapを指定しています。 そしてfakeResult型はFetcher()の戻り値と同じbodyとurls。 これは名前の通り、課題用のダミーのフェッチ結果を返すための実装ですね。 fakeFetcher型のFetch()実装は、自身のmapにURLをキーとする項目があるばそれを返し、無ければ"not found"を出力。

そして、ダミーデータを渡してfakeFetcherを取得。 ダミーデータは、URLをキーにして、bodyと次階層のurlsを持ったfakeResultを持つ。

// 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/",
        },
    },
}

これで課題用クローラの初期コードは読み終わりました。 この初期コードを実行すると、ダミーデータから該当したURLとbodyが出力されます。

で、課題の改良は、クロールの並列化と重複の防止。 当然goroutineを使いますが、どこにどう入れるべきか。 悩んだりこねくり回した結果、こうなりました。

package main

import (
    "fmt"
    "sync"
    "time"
)

var fetched = make(map[string]bool)

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

type crawlResult struct {
    url  string
    body string
    urls []string
    err  error
}

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
    ch := make(chan crawlResult, 1)
    var wg sync.WaitGroup
    wg.Add(1)
    go func(){
        wg.Wait()
        close(ch)
    }()
    go crawlWorker(url, depth, fetcher, ch, &wg)
    for r := range ch {
        if r.err == nil {
            fmt.Printf("found: %s %q\n", r.url, r.body)
        } else {
            fmt.Printf("not found: %s\n", r.url)
        }
    }
}

func crawlWorker(url string, depth int, fetcher Fetcher, ch chan crawlResult, wg *sync.WaitGroup) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
// fmt.Printf("%s", wg)
    defer wg.Done()
    if depth <= 0 {
        return
    }
    if fetched[url] {
        return
    }
    fetched[url] = true
    body, urls, err := fetcher.Fetch(url)
    r := crawlResult{url, body, urls, err}
    ch <- r
    if err != nil {
        return
    }
    for _, u := range urls {
        wg.Add(1)
        go crawlWorker(u, depth-1, fetcher, ch, wg)
    }
    return
}


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

// 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) {
    time.Sleep(300 * time.Millisecond)
    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/",
        },
    },
}

これまで学んできた範囲で解決できなかったのは、channelによるクロール結果待ちが、goroutineのデッドロックを伴う点。 クロールを行う各goroutineからのメッセージは、無限ループでchannelを待って受け取りますが、全てのクロールが完了した時点でchannelを閉じる必要があります。 閉じなければ、channelが発生させるロックが宙に浮き、デッドロックとなってしまいます。 しかし、goroutine側ではどの時点でchannelを閉じるべきか判断できません。

あれこれ調べて、下記の記事に行き着きました。

悩んでいたそのものへの言及があり、syncパッケージを使うことで解決できそうだと判断しました。 syncのAdd(),Done(),Wait()を使って、wait用のgoroutineを無名関数で走らせて上手くいくはずが、なぜか変わらずデッドロックが発生。 しばらく考えてプリントデバグした結果、クロールのgoroutine側でwg.Add()したはずが、カウンタが増えていない。 WaitGroupをクローラの各goroutineに渡す際、値渡しになっていたのが原因でした。 結局ここをポインタによる参照渡しに変え、デッドロックは回避。

チュートリアルの課題で出てきていないパッケージであり、そんなもので解決するのは間違っている気もしますが、薄くとはいえgoroutineのイメージが少し深まったので良しとします。

重複するURLの取得回避にグローバルに置いたmapを使うとか、なんか色々問題はありそうですが、ひとまずこれで課題クリア。

72.Where to Go from here...

これでツアーは終了です。長かったような短かったような。

ツアー中ずっとgopherくんが見ていてくれたのは楽しかった。

課題はありがたいけど、goらしい書き方というのが身についているのか不安で、取り組んだ後には解答例が欲しくなりました。

ツアーの中で触れられていない基本的な要素も多数ありそうなので、次も何かチュートリアルを見てみようと思います。

ゼロから学ぶGo言語プログラミング(15) A Tour of Go 67~70

f:id:belbo:20140616112930j:plain

A Tour of Goの続き。

67.Selcet

selectは、switchのようで違う、goroutine用の制御構造。

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

こうあるので、selectのcaseにはchannelが必須ということでしょうか。

複数のcaseが成立した場合、全てでも先頭でも末尾でもなく、ランダムに実行されるcaseが決まるというのは、使い所があるんでしょうか。

68.Default Selection

これもswitchと同じで、caseとdefaultで制御。

通信のレスポンスとか待機時間やステータスが色々な場合に、selectで待ち構えておいて、defaultで待機処理したりに使えるのかな。

構文の性格上、fallthrough は不要だから無さそう。

69.Exercise: Equivalent Binary Trees

70.Exercise: Equivalent Binary Trees

goroutineのまとめっぽい課題。

"Binary Trees"で、バイナリデータを持ったツリー?と勘違いしてしまったが、二分木のことだった。 ツアーの解説の中では二分木という表現の方が良いと思う。

課題の内容は、"code.google.com/p/go-tour/tree"パッケージによって、二分木を生成するtree.New()が提供され、この二分木を比較して同一かを判定する、というもの。

二分木の比較をするためには、ツリーを辿って探査していかないといけないのか。 Walk関数でそれを実装しろとなっている。 与えられる二分木はこういう構造体。

type Tree struct {
    Left  *Tree
    Value int
    Right *Tree
}

Treeは自身の持つ値を返す Value()を持ち、LeftやRightは子のTreeへのポインタ。 ということは、ルートから順に値を取り、LeftやRightの存在を確認して、存在すれば次のノードへ、という方法で走査できそう。

Walk()にchannelを渡すのだから、go Walk()という風にgoroutineで動かすのだろうと試すと、"runtime error: invalid memory address or nil pointer dereference"のpanicが。 tにツリーが渡される前提になっていたのが原因だったので、t == nil でWalk()を終了することに。 というようなのを繰り返し、なんとか動く形にはなった。

chaanelでの受信を条件にしたループの書き方が分からなかったけど、rangeでいけるらしい。

他に、Walk()ひとつを呼ぶだけでは、close()のタイミングが持ちようがなく、結局Walk()とwalkNode()という2つの関数を設置した。

package main

import (
    "code.google.com/p/go-tour/tree"
    "fmt"
)

// Walk walks the tree t sending all values
// from the tree to the channel ch.
func Walk(t *tree.Tree, ch chan int) {
    walkNode(t, ch)
    close(ch)
}

func walkNode(t *tree.Tree, ch chan int) {
    if t == nil {
        return
    }
    if t.Left != nil {
        walkNode(t.Left, ch)
    }
    ch <- t.Value
    if t.Right != nil {
        walkNode(t.Right, ch)
    }
}

// Same determines whether the trees
// t1 and t2 contain the same values.
func Same(t1, t2 *tree.Tree) bool {
    c1 := make(chan int)
    c2 := make(chan int)
    go Walk(t1, c1)
    go Walk(t2, c2)
    for n1 := range c1 {
        n2 := <-c2
        if n1 != n2 {
            return false
        }
    }
    return true
}

func main() {
    fmt.Println(Same(tree.New(1), tree.New(1)))
    fmt.Println(Same(tree.New(1), tree.New(2)))
}

相変わらずこれがGoらしいコードなのか、そして根本的な使い方など誤っていないのかは分からない。 でもとりあえず、goroutineに少しだけ慣れられた気がする。

ゼロから学ぶGo言語プログラミング(14) A Tour of Go 62~66

f:id:belbo:20140616112930j:plain

A Tour of Goの続き。

62.Concurrency

やっとGoのうりのひとつ、並行性。

63.Goroutines

goroutine (ゴルーチン)は、Goのランタイムに管理される軽量なスレッドです。

Goはgoroutineという軽量なスレッドを簡単に生成できて、任意の関数を

go f()

とするだけで、新たなgoroutineで実行できる。

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

これのsay()からforを取り除くと、"hello"のみが出力される。 あるいはmain()でどちらのsay()もgo say()とすると、何も出力されない。

fmtに関するメモリの概念を理解できていないからわからないんだろうか。

64.Channels

このchannelが、並行処理の同期の良い方法らしい。

とりあえず channel は次のようなもの。

  • 値の送受信を行うための直通ルート
  • 送受信には"<-"という記号を使用し、これをチャンネルオペレータと呼ぶ
  • channelは make(chan 型)というように、makeでの生成が必要
  • 引数にchannel型を指定する場合は、"f(c chan int)"のように書く

概念はなんとなくは分かるけれど、サンプルコードにはchannelが複数回出てきて混乱した。 でも、channelはあくまで経路でしかないと考えれば、すっきりした。

つまり生成したchannelをgoroutineに渡し、goroutine内では結果をreturnではなく、そのchannelに渡す。 goroutineの外のコードでは、そのchannelを引数に取る処理があるが、channelからデータが送られてくるまでは待機する、という感じらしい。

ということで、自分でchannelを使ってみる。

package main

import (
    "fmt"
    "time"
)

func think(x int, y int, t int, c chan int) {
    time.Sleep(time.Duration(t) * time.Millisecond)
    c <- x + y
}

func main() {
    c := make(chan int, 2)
    go think(5, 5, 1000, c)
    go think(10, 10, 500, c)
    fmt.Println(<-c)
    fmt.Println(<-c)
}

んー、なんとなくは分かったけれど、どういう基準でchannelを作るべきかとか、どういう状況で利用すべきかはまだ見えない。

65.Buffered Channels

channelはバッファサイズを指定して生成でき、バッファサイズを持つchannelをBuffered Channelsと呼ぶらしい。

バッファの単位は解説されていないけれど、channelに対する送受信単位の回数ということだろうか。

バッファが使えると、送受信のそれぞれがバッファを緩衝にして、送信が追いつかなければ受信はバッファから受信するか、バッファが空なら待機。 受信が追いつかなければ、送信はバッファにデータを積み、バッファもいっぱいであれば待機、という使い方ができるのかな。

66.Range and Close

channelを閉じるcloseは、シンプルに

close(chan)

とだけ。

閉じたchannelは受けてで検出。 もし閉じたchannelに送信すると、pannicというランタイムエラーの一種になる、と。

チャネルは、ファイルと同じようなものではありません。 通常は、閉じる必要はありません。

これ、作ったchannelはいつ破棄されるんだろう。 明示的に手動で破棄?

ゼロから学ぶGo言語プログラミング(13) A Tour of Go 61

f:id:belbo:20140616112930j:plain

A Tour of Goの続き。 今回は長くなったので、io.Readerの課題のみ。

61.Exercise: Rot13 Reader

画像に引き続き、組み込みのインターフェースを元に、独自の実装を行う課題、かな? 課題そのものはio.Reader型を元にして、ストリームにROT13の複合を施す独自の型を作る、というもの。

バイト列やストリームの扱いも、ファイルの処理をあまりして来なかったため曖昧。 Goはちょっとしたツールを作るのがとても楽を出来そうなので、そういう点でもこの辺りは早めに理解しておきたい。

ROT13はぼんやりしか理解してなかったので、解説からもリンクがあるROT13 - Wikipediaで確認。 ROT13はアルファベットのみを対象として、ABC順に13文字後ろへずらす、という処理。 アルファベットは全体で26文字なので、2回の適用で元の文字に戻る。 暗号化と複合が同一処理で、しかもそれは非常に単純な変換の場合、暗号と言っていいんだろうか。

この処理を実装するには、対象の文字がアルファベットであるかどうか判定し、そうであれば13文字ずらした結果を得る必要がある。 Goでのこの手の文字判定や処理は全然なので、色々調べる必要がありそう。

io.Readerインターフェースを確認すると、byteの並んだsliceを受け取って、intとerrorを返す、となっている。 課題で扱うのはアルファベットだけど、バイト列を扱うから[]byteなのか。

importには最初から strings パッケージが並んでる。 このパッケージはstrings.NewReader()が使われている。 io.Reader自体はバイト列を扱う汎用的なもののようなので、strings.NewReader()は恐らく、文字列のバイト列を扱うものっぽい。

インターフェースのコードだけでは、実装すべき戻り値 int や error の仕様がわからない。 解説では n は受け取ったバイト列の長さを けど、rot13Readerはio.Readerを内包していて、main()でもROT13された文字列を取ったstrings.NewReader()が引数になっている。 つまり r には文字列のバイト列を受け取ったio.Readerが渡されるわけだから、とりあえずこれをそのままスルーしてみる。

func (r13 *rot13Reader) Read(s []byte) (n int, e error) {
    n, e = r13.r.Read(s)
    return
}

すると"Lbh penpxrq gur pbqr!"と、ROT13された課題の文字列がそのまま出力された。 この時点で s には渡されたROT13のバイト列が入っている。 バイト列から位置文字ずつROT13での変換をかけて返せば、仕様は満たせるはず。 しかし、Goでそういった文字列の比較や変換をどうおこなうべきか分からないので、幾つか基本そうなコードをPlaygroundで書いて試してみる。

まず、文字同士の比較。

package main

import "fmt"

func main() {
    x := "c"
    y := "b"
 
    f := "\"%s\" > \"%s\" is %s."
    if x > y {
        fmt.Printf(f, x,y,"true")
    } else {
        fmt.Printf(f, x,y,"false")
    }
}

単純に文字同士の比較では、シンプルに大小が判定できました。 では、文字を変更してみます。

"あ" > "b" is true.

ひらがなや漢字でも、適切に判定できています。 Unicodeベースで扱われているんでしょうか。 ともかくこれで、渡された文字がアルファベットに含まれるかは、

c >= "A" && c <= "z"

で判定できることがわかりました。 しかしここからどう文字を13ずらすかに悩んで言語仕様を読み直すと、こんな内容が。

文字リテラル - Go言語仕様 - golang.jp

"日本語" // UTF-8 input text 日本語 // UTF-8 input text as a raw literal

なんと、Goではクォートの違いで文字のリテラルの扱いが違う。 シングルクォートの場合、Unicodeのコードポイントを表現する。 ダブルクォートの場合、Unicode文字列となる。

ということは、シングルクォートの文字リテラルは、Unicodeのコードポイントを加減算できるのでは?

package main

import "fmt"

func main() {
    x := 'A'
    fmt.Println(string(x+1)) // output "B"
}

シングルクォートで文字リテラルは、そのままだとUnicodeのコードポイント。 stringで文字列として出力すれば、Aから+1でBになっている。 これを使えばROT13も可能そうだ。

最終的に、このコードで課題の仕様を満たせた。

package main

import (
    "io"
    "os"
    "strings"
)

type rot13Reader struct {
    r io.Reader
}

func (r13 *rot13Reader) Read(s []byte) (n int, e error) {
    n, e = r13.r.Read(s)
    for i:=0; i < n; i++ {
        if s[i] > 'A' && s[i] < 'z' {
            if s[i] > 'z' - 13 {
               s[i] = s[i]-13 
            } else {
               s[i] = s[i]+13 
            }
        }
    }
    return
}

func main() {
    s := strings.NewReader(
        "Lbh penpxrq Gur pbqr!")
    r := rot13Reader{s}
    io.Copy(os.Stdout, &r)
}

しかし理解できていないところもちらほら。

Stringやbyteの関係。

なぜ末尾で"io.Copy(os.Stdout, &r)"としているのか。

結局、ストリームをきちんと理解しきれていないのが問題。 自分の課題として残し、今は先に進む。