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

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)とは色々と違ってくるので、最初に目を通しておくと良い。

もう少し効率的かつ優しいのが欲しいなら、先人が残してくれてる日本語情報を。

ただ最初から全部読んでもピンと来ないので、手を動かしながらの方が良い。

  • 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) {
  • C言語時代に散々苦しめられたポインタの概念がある
    • ただ Python で Shallow/Deep copy に親しんでたので今はあまり抵抗はない
  • 標準出力系が fmt.XXXXX と書かないといけないのが少しだるい