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の解説記事よりも、もっとターゲットやペースがはっきりしているからか、進めやすくもある。

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