Golang で Markdown TOC 作成ツールを作り直してみた
ここ数週間くらいは Golang を触っていた。stakiran/intoc という Markdown から TOC をつくる Python 製ツールがあって、これを Golang で実装し直すことで学習を図った。
ここまで学んだことをざっくりまとめておく。
前提
- Windows
- エディタは軽いやつ(私は秀丸エディタ)を使う
- 1KL 以内の小さなコードを書くことのみ想定
- IDE は用意しない
- コード例はたまに Python との比較を出す(ので知らないなら無視すること)
環境構築
言語環境のインストール
公式サイト Downloads - The Go Programming Language からダウンロード。インストーラーになっている。インストール先は気に食わないなら変えて良い。私は D:\bin1\Go にした。
インストール後は環境変数 GOPATH と GOROOT を設定する。私は以下のようにした(go env で環境情報を見れる)。
$ go env ... set GOPATH=D\bin1\gopath ... set GOROOT=D\bin1\Go ...
併せて環境変数 PATH に %GOPATH%\bin を通す。
パッケージのインストール
go get (URL)
コマンドを使う。プロキシ環境下なら環境変数 HTTPS_PROXY と HTTP_PROXY に事前にプロキシを設定しておくこと。
go get を実行するとファイル群は GOPATH 配下に保存される。GOPATH 配下は以下の構造になっている。
D:\bin1\gopath | +--- bin コマンドとして実行できるものはここに入る | +--- pkg binに入れてる実行ファイルを作る時の生成物(コンパイル時の生成物) | +--- src 開発時に import して使う分はここに入る
パッケージがコマンドとして提供されたものなら(binディレクトリに実行ファイルが入るので)、どのディレクトリからでもコマンドを実行できるようになるはず。
パッケージがライブラリとして提供されたものなら、import 文でインポートできる。
go get 例: テストフレームワーク testify
入手:
$ go get github.com/stretchr/testify
使用:
package main import ( "github.com/stretchr/testify/assert" "testing" ) func TestAdd(t *testing.T) { assert.Equal(t, 1, 10-8-1) }
要するに go get した URL と同じ URI を import 文で指定する。ここでは assert 部分のみ import しているが。
Hello World
hello.go
package main import "fmt" func main() { fmt.Println("Hello, 世界") }
実行
実行は go run hello.go。これでコンパイルが走ってテンポラリフォルダ( %temp% )に実行ファイルができた後、それが実行される。
ビルド
ビルドは go build hello.go。これで hello.exe が生成される。
フォーマッティング
go fmt コマンドでソースコードの書式整形を行う。
go fmt hello.go を実行すると、hello.go が Go のコーディングスタイルに整形される。
インデントはタブ派だスペース派だとか、{
の位置とか、そういうので論争する余地はない。ただ「go fmt を通せ」で済む。
コーディング全般
他言語(少なくとも私が知っている Python, Javascript)とは色々と違ってくるので、最初に目を通しておくと良い。
もう少し効率的かつ優しいのが欲しいなら、先人が残してくれてる日本語情報を。
- Go 言語に関するブックマーク - Qiita
- 他言語プログラマがgolangの基本を押さえる為のまとめ - Qiita
- golang チートシート - Qiita
- 逆引きGolang
- Goプログラミング言語仕様 - golang.jp 古い
- avelino/awesome-go: A curated list of awesome Go frameworks, libraries and software
- Go言語の初心者が見ると幸せになれる場所 #golang - Qiita
- Introduction · Go Web プログラミング
ただ最初から全部読んでもピンと来ないので、手を動かしながらの方が良い。
- A Tour of Go
- チュートリアル
- 根気があるならこれでいいかと
- 退屈でつまらない(言語フェチとかなら楽しめる気はするが)
- The Go Playground
- ブラウザだけで実行できるのでちょっとした学習や試行に最適
ちなみに私は拙作の stakiran/intoc: TOC generator for Markdown. これを Go で書き直してみることで学習をした。intoc はファイル I/O、パス操作、引数解析、リスト操作、文字列操作、クラス、Markdown 構文のパース等を含んでいるため、(各種処理を Go でどう書くかという)基礎を一通り学べると目論んだ。
ユニットテスト
go test コマンドを実行する。
go test は、testing パッケージベースで書かれた「XXXXX_test.go というファイル」の「TestXXXXX という関数」を全部実行していく。
go test を実行する時は go test intoc_test.go intoc.go
のように「テストコード」と「テストコードが読みに行くソース」の両方をコンパイル対象に含める必要がある。.go をちゃんと書いてるなら go test ./
みたいにディレクトリごと指定しちゃうのもアリ。
テストコード
典型的には以下のようになる。
- テスト対象ソースへのアクセスは...
- import する
- 対象ソースと同じパッケージにする(下記ソースはこれ。main パッケージにしている)
- testing パッケージを import する
- アサーション関数等はないので、自分で actual と expect を比較して、違うなら Errorf でエラーを出す、みたいなことをやる
package main import ( "testing" ) func TestAdd(t *testing.T) { actual := 10 expected := 2 + 8 if actual != expected { t.Errorf("actual %v\nwant %v", actual, expected) } } func TestSub(t *testing.T) { actual := -5 expected := 2 - 8 t.Skip("skip sub test.") if actual != expected { t.Errorf("actual %v\nwant %v", actual, expected) } }
ただしこれでは最低限すぎて面倒くさいので、テスト用のパッケージを使った方が楽。
私は今回 stretchr/testify を使った。testify を使うと assert.Equal とか assert.True とか使える。他にも mock とか色々あるみたいだが。
文字列操作
基本的に strings パッケージに頼るので import しておくこと。あと書式整形(C言語でいう printf)系は fmt パッケージを使うので、必要なら import しておく。
format
s := fmt.Sprintf("%s-%d", ret, dupCount)
参考: 忘れがちなGo言語の書式指定子を例付きでまとめた - Qiita
utf8 string の先頭 1 文字を slice する
Go では string は bytestring なので、そのまま slice しても「文字」単位が正しく認識されない。
以下のように、いったん []rune
にキャストしてから slice する。slice 後は、string で使うためにキャストで戻すことも忘れずに。
peekfirst = string([]rune(line)[:1])
Python2 時代を思い出す。
replace
ret = strings.Replace(ret, " ", "-", -1)
第4引数は置換回数。0 未満なら無限回。
lower
ret = strings.ToLower(ret)
startswith, endwith
startsWith := strings.HasPrefix("prefix", "pre") // true endsWith := strings.HasSuffix("suffix", "fix") // true
strip
ret = strings.TrimSpace(ret)
"a"*32 ← こういう繰り返し
strings.Repeat("a", 32)
リスト操作
Go にはリスト(list)は無い。あるのは配列(array)。
loop/scan(各要素を走査する)
[]string 型の lines 配列(文字列配列の lines 変数)があるとして。Python の enumerate に近い書き方。
for i, line := range lines { fmt.Printf("%d: %s\n", i, line) }
添字 i を使わない場合は、_
で伏せる。伏せないとコンパイルエラー not used で怒られる。
for _, line := range lines { fmt.Println(line) }
join(array を string にする)
strings.Join(tocLines, "\n")
split(string を array にする)
strings.Split(aString, "\n")
append
s := []int{} s = append(s, 1) s = append(s, 2, 3, 4)
extend(array に別の array を concat する)
append 対象 b を b
ではなく b...
の形で与える。
package main import ( "fmt" ) func main() { a := []string{"a","b","c"} b := []string{"1","2","3"} a = append(a, b...) }
参考: append - Concatenate two slices in Go - Stack Overflow
引数解析
Go 標準で簡単なのが flag パッケージ。
大まかな流れは、
// オプションを定義していく. // これは文字列値の -input オプションを定義した例. argInputFilepath := flag.String("input", "", "An input filename.") ... // 与えられたコマンドラインの解析を走らせる. flag.Parse() // 定義した分に実際の値が入るので適宜アクセス. // ポインタになっているので注意. fmt.Printf("Input filepath is '%s'.\n", *argInputFilepath) ...
required(必須パラメーター)
flag では実現できないので、自力で頑張る。
以下は(上例の) -input オプションが与えられなかった時に「必須ですよ、的なメッセージ」「Usage 表示」して終了する例。
if *argInputFilepath == "" { fmt.Println("-input required.") flag.PrintDefaults() os.Exit(2) }
与えられたオプションすべてを標準出力する
flag の VisitAll を使う。VisitAll には(渡されてくる各オプションをどう処理するかを定義した)関数を渡してあげる感じ。
以下は -debug-print-all オプションが指定された時に、オプション全部を表示する例。
argDebugPrintAll = flag.Bool("debug-print-all", false, "[DEBUG] print all options with name and value.") ... printOption := func(flg *flag.Flag) { fmt.Printf("%s=%s\n", flg.Name, flg.Value) } if *argDebugPrintAll { fmt.Println("==== Options ====") flag.VisitAll(printOption) }
flag.Flag 型について flag - The Go Programming Language を。
ファイルパス走査
Big Sky :: Golang で物理ファイルの操作に path/filepath でなく path を使うと爆発します。 が詳しい。
所感
総合
良い。
小物ツール(特にバイナリで配布したいものや性能が欲しいもの)は今後 Go で開発していきたい。また純粋なマンネリ防止や娯楽としても、あえて(慣れてる Python ではなく)Go で書いていきたい……かな。
とりあえず Tritask の Go 化はやりたい。そしたらバイナリ化も楽にできて普及啓蒙が楽になる。性能も出て、今まで 1 万件程度(で秒時間がかかってしまっていた遅さ)だったソート処理も 10 万件くらいまで扱えるようになるだろう。
良い
- go get、go run、go test、go env など簡潔なコマンドで完結するので快適
- go run コンパイルでつまらん構文エラーを検出できるので楽
- go build だけで実行ファイル化できる気軽さが嬉しい
- Python だと実行ファイル化は難題だったので……
- 実行ファイルの動作が高速で素晴らしい
- intoc も Python スクリ版と比べると 10 倍は軽く違う印象
- 型推論が働くので
aString := "aaaaaa"
で済むのが楽- いちいち
var aString string = "aaaaaaa"
と書かなくていい
- いちいち
- 標準ライブラリ充実してて、小物ツール書く程度なら困らないと思う
- ググったら大体答え出てくる
- ググる時は
go
ではなくgolang
が良い
- ググる時は
つらい
- unicode string を扱うのにいちいち rune が必要
- OOP をサポートしていない
- 構造体 + レシーバ(この構造体にこの関数を紐付ける、的な概念)である程度は模倣できる
- あと interface なるものもある(まだ試してない)
- 型の指定に伴って記号が増えるので最初は「ウッ」となる
- 例1:
lines := []string{"a", "aaa", "aaaa"}
- 例2:
func createOutLines(lines []string, tocLines []string, args Args, editTargetPos int) ([]string) {
- 例1:
- C言語時代に散々苦しめられたポインタの概念がある
- ただ Python で Shallow/Deep copy に親しんでたので今はあまり抵抗はない
- 標準出力系が
fmt.XXXXX
と書かないといけないのが少しだるい