Goの勉強がてらにPythonで書いたCLIをGoで書き直した話
こんにちは。社会人2ヶ月が経過したたんごです。 社内研修も終わり部署配属されてはや2週間が過ぎました。本当に早いw
さてさて、部署配属されてからほとんどRubyばっかり書いていてPython触っていないんですが最近RustとGoに興味が出てきまして、せっかくなので少し前に書いたCLIをGolangで書き直してGoを学ぼうという魂胆です。
- 前提条件
- 文法を学ぶ
- Range
- CLI の設計
- JSONの読み込みと解析
- APIに実際に接続
- CLIの完成
- CircleCI 2.0を使ってテストする
- テストカバレッジの計測
- Windowsで動作を保証するためにAppveyorを使う
- ここまでで役に立った資料、手法など
- 疑問
- まとめ
前提条件
こう書くだけだと2つしかやること無いし楽じゃんと思いましたがそううまくは行きませんでした。
文法を学ぶ
まず、Goのデータ構造と文法を知らないのでそこを学ぶ必要がありました。 とりあえず公式にあるA Tour of Goをそのまま読み進めていきました。Pythonで書いていたときと所々異なる点があったのでそれだけメモしておきます。(とはいえ公式を読めばわかるんだけどね)
基本的には A Tour of Go をそのまま読み進めていくものです。ただ個人的に気になったことやメモを残しておこうと思い記事にしました。
配列
Pythonだと何も考えずに [ ]
に値を放り込むだけですが、Golangでは配列の長さを1度決めたら変えることはできないし、型も決まっています。
定義の基本
var <変数名> [長さ]<型>
また、2通りあります。1つは var
で型を確定させて定義するもの。また、2番めのように初期値をそのまま決め打ちで初期化することもできます。
var a [2]string a[0] = "Hello," a[1] = "world" primes := [6]int{2, 3, 5, 7, 11, 13}
スライス
配列は固定長のサイズであるのに対し、スライスは可変長サイズです。容量が足りなくなると自動的に領域を広げてくれます。
生成にはビルトイン関数の make
を使います。
a := make([]int, 5)
また、スライスに値を追加したい場合は append
という関数を用います。append
関数は第1引数が値を追加したいスライスを入れ、残りの引数は追加したい値を入れます。
package main import "fmt" func main() { var s []int s = append(s, 0) printSlice(s) s = append(s, 1, 2, 3, 4, 5) printSlice(s) } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) }
実行結果は以下のようになる。
len=1 cap=1 [0] len=6 cap=6 [0 1 2 3 4 5]
Pythonでは、[1, 2].append(10)
のようにしますがGoはオブジェクト指向言語ではなく命令形言語なのでこうなります。
Range
RangeははPythonで言うところの enumerate と同じ挙動をしてくれます。雰囲気的にはスライスにアクセスする際にイテレータ操作ができるすぐれものです。
Pythonでこのような処理は
pow = [1, 2, 4, 8, 16, 32, 64, 128] for i, v in enumerate(pow): print("2**{i}={v}".format(i=i, v=v))
Golangではこのように書けます。
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} for i, v := range pow { fmt.Printf("2**%d=%d\n", i, v)
辞書
Pythonではdict型をGolangではMapといいます。用語の違いなので覚えるしかなさそうです。
スライス同様で make
関数でも定義できます。
Goではmapはミュータブルなものであるので、最初にmapを定義してしまえば値を挿入したり、キーによる参照や削除が可能になっています。
package main import "fmt" func main() { m := make(map[string]int) // stringがキー。値はint m["answer"] = 42 fmt.Println("The value: ", m["answer"]) // 取得 m["answer"] = 28 fmt.Println("The value: ", m["answer"]) // 上書き delete(m, "answer") fmt.Println("The value: ", m["answer"]) // 削除 v, ok := m["answer"] fmt.Println("The value: ", v, "Present?", ok) // 存在するか }
削除した後に参照すると例外等は発生せず、初期値が変えるようになっています。また第2引数で存在可否が判定できます。
Exercise: Maps
A Tour of Goの練習問題として文字列s
に含まれる単語を数え上げるプログラムを書きました。
package main import ( "strings" "golang.org/x/tour/wc" ) func WordCount(s string) map[string]int { m := make(map[string]int) for _, key := range strings.Fields(s) { _, exist := m[key] if exist { m[key] += 1 } else { m[key] = 1 } } return m } func main() { wc.Test(WordCount) }
ちなみに実行結果はこんな感じになります。
$ go run aaa.go PASS f("I am learning Go!") = map[string]int{"I":1, "am":1, "learning":1, "Go!":1} PASS f("The quick brown fox jumped over the lazy dog.") = map[string]int{"jumped":1, "over":1, "lazy":1, "dog.":1, "The":1, "quick":1, "brown":1, "fox":1, "the":1} PASS f("I ate a donut. Then I ate another donut.") = map[string]int{"I":2, "ate":2, "a":1, "donut.":2, "Then":1, "another":1} PASS f("A man a plan a canal panama.") = map[string]int{"man":1, "a":2, "plan":1, "canal":1, "panama.":1, "A":1}
関数
Golangの関数には引数に関数を与えることができるよという話でした。またGoの関数はクロージャらしいです。
例題としてフィボナッチ数列をクロージャを使って書いてみました。
package main import "fmt" func fibonnacci() func() int { p, n := 0, 1 return func() int { p, n = n, p+n return n } } func main() { f := fibonnacci() for i := 0; i < 10; i++ { fmt.Println(f()) } }
こんな感じ。
メソッド
Goにはクラスがないため、メソッドは存在しないはずです。けれども型にメソッドを定義できるという話みたいです。このときはfunc
キーワードとメソッド名の間に引数リストを書きます。これらをポインターレシーバーといい、ポインターレシーバーがないと、自分の型に定義したメソッドで値を書き換えることができません。
例えばA Tour of Goのコードを拝借して自分なりの言葉でまとめます。
package main import "fmt" type Vertex struct { X, Y float64 } func (v *Vertex) Scale(f float64) { v.X = v.X * f v.Y = v.Y * f } func main() { v := Vertex{3, 4} v.Scale(10) fmt.Println(v) }
Scale
は型 Vertex
のメソッドで、引数に10を与えると結果は次のようになります。
{30 40}
これに対し、もし変数レシーバーとした場合、つまり
- func (v *Vertex) Scale(f float64) { + func (v Vertex) Scale(f float64) {
のように書くと次のような結果が得られます。
{3 4}
つまり、変数レシーバーの場合は元のVertex変数のコピーを操作しているため、呼び出し元の値は変更されません。
interface
最初公式ドキュメントを読んだときは全く意味がわからなかったのですが、簡単に言うと自分で好きな型を決めることができるみたい。
後々JSONをマッピングする際にこれを使いまくります。多分ですが、The empty interfaceから先は読んでおいたほうがいいと思います。
その他
今回は使いませんでしたが、Go Routine等もきちんと勉強しておくと並行処理をする際に役立つかもしれません。
さて、Go言語の大体の学習にPythonとの比較を踏まえてやっていたため、だいたい2時間程度を要しました。ここからは実際にCLI形式のプロジェクトをPythonからGoに書き直していきます。
CLI の設計
移植前のPythonのコードはこちらになります。
挙動としてはREADMEに書いてあるとおりですが、factordb という数字を投げつけると素因数分解の結果を返してくれるサイトがあります。(ちなみに毎度素因数分解を実行しているのではなく、DBというようにデータベースから既知の値を返してくれるだけです) 例えば、16なら 2 ^ 4 のように返してくれます。CLIでは次のように使います。
$ factordb 16 2 2 2 2
また、オプションに --json
をつけるとAPIからの返り値を使いやすいJSONの形式に変形して出力してくれ、かつもう少し詳細を出してくれます。
$ factordb --json 16 {"id": "https://factordb.com/?id=2", "status": "FF", "factors": [2, 2, 2, 2]}
のコードを参考に書きました。
最初に↑を参考にオプションを引き取って適当にCLIっぽい挙動をするコードを書きました。
package main import ( "fmt" "os" "github.com/codegangsta/cli" ) func main() { app := cli.NewApp() app.Name = "factordb" app.Usage = "The CLI for factordb.com" app.Version = "0.0.1" // Global option app.Flags = []cli.Flag{ // --json cli.BoolFlag{ Name: "json", Usage: "Return response formated JSON", }, } app.Action = callAction app.Run(os.Args) } func callAction(c *cli.Context) error { var isJson = c.GlobalBool("json") if isJson { fmt.Println("Response json type") } var paramFirst = "" if len(c.Args()) > 0 { paramFirst = c.Args().First() } fmt.Printf("Hi, I am receiving the number %s\n", paramFirst) return nil }
https://github.com/ryosan-470/factordb-go/blob/acd2fbb6c5e9bf7f84e7c2bd7e90001c3337b012/cli.go
↑のコードはもし、--json
がついていると標準出力に Response json type
と、あとは引数をHi, I am receiving the number 16
みたいな形で出力してくれるプログラムです。このままどの環境でも動かせると思います。
JSONの読み込みと解析
次に、JSONを読み込んでGolangの構造体に置き換えます。Golangでは
などにあるように、json.Unmarshal
などで解析して構造体に置き換えることでコード内でよしなに扱うことができるみたいです。しかしFactorDBのAPIにアクセスしてみればわかりますが返ってくるJSONはお世辞にもキレイなものとはいえません。
$ http http://factordb.com/api\?query\=10 HTTP/1.1 200 OK Connection: Keep-Alive Content-Encoding: gzip Content-Length: 67 Content-Location: api.php Content-Type: application/json Date: Wed, 07 Jun 2017 09:23:37 GMT Keep-Alive: timeout=5, max=100 Server: Apache TCN: choice Vary: negotiate,Accept-Encoding { "factors": [ [ "2", 1 ], [ "5", 1 ] ], "id": "10", "status": "FF" }
この factors
をどう表せばよいのかわからずつまりました。そこで色々調べてみると次のようなブログに当たりました。
書いてある通りで申し訳ないのですが、何も考えずに interface
に投げ込めばいいみたいです。あとは前学んだように自分でよしなに型を変えていきます。今回は2つの構造体を用意しました。
type FactorDB struct { Status string Id string Factors []Factor } type Factor struct { Number int Power int }
FactorDB
構造体は返ってくるレスポンスを表しています。Status
や Id
は string
で問題ないでしょう。Factors
だけはArrayで返ってくるので自分でさらに中身を定義し直します。
公式APIドキュメントなど存在しないので自分で勝手に解釈していますが、factors
は文字列の方を残りの数でべき乗するというような形になっていると推定しましたw なのでこのような構造体で扱えると思います。
最終的にJSON文字列を引数にとり、FactorDB
構造体を返すGoのプログラムは次のようになりました。
func ConvertToFactorDB(b []byte) FactorDB { var base interface{} err := json.Unmarshal(b, &base) if err != nil { log.Fatal("Cannot parse your input") } s := base.(map[string]interface{}) var factor FactorDB factor.Status = s["status"].(string) factor.Id = s["id"].(string) factors := s["factors"].([]interface{}) for _, f := range factors { tmp := f.([]interface{}) number, _ := strconv.Atoi(tmp[0].(string)) power := int(tmp[1].(float64)) factor.Factors = append(factor.Factors, Factor{number, power}) } return factor }
(こんな感じで簡単に書いてありますがだいたいここまでに3時間を要しましたw)
また、さりげなくですが、packageを分けました。まだGoのパッケージの切り方があまり良くわかっていませんが
How to Write Go Code - The Go Programming Languageの Your first library という部分を参考に切り分けました。
書いてある通りに $GOPATH/src/github/ryosan-470/factordb-go
のような場所にファイルを置き、go build
で何も文句が言われなければとりあえず OK というスタンスで行きますw
テストコードを書く
最近、コードを書くときにテストを追加していくというのが私の中で流行っているので、テストも書きましょう。
同様にHow to Write Go Code - Testingの章を見て、書きました。
Goには assert
のような関数はなく、比較演算子 ==
も基本型以外では動作しないようでしたので、自分で比較するような関数を別途テストに組み入れます。
- https://github.com/ryosan-470/factordb-go/blob/af37df2ea6527c3e02aae1140dfaeee69844a28e/handler/response_handler_test.go
- https://github.com/ryosan-470/factordb-go/commit/1fb069da61dba247e61add1673f6d2cbeea481d7
APIに実際に接続
次に実際にAPIに接続し、リクエストを送って、先程↑で作った構造体に直してやるプログラムを書きます。
なお、先程の FactorDB
構造体は汎用的すぎたため FactorDbResponse
という構造体になっています。
https://github.com/ryosan-470/factordb-go/commit/c928408522e91d8dc22888c4f9a5bd82181f37b9
さて、まず構造体の定義をしました。
type FactorDB struct { Number int Result handler.FactorDbResponse }
こうすることでmain関数内で先に var factordb FactorDB
として初期化し、CLIで第1引数として数字を受け取ったあとに factordb.Number = 16
みたいな形で、値を保持できます。
Pythonで言えばこんな感じで使いたいという気持ちだけでも伝わってほしいですw
class FactorDB(): def __init__(self, n): self.n = n self.result = None
あとはGoから実際に接続して値を取得しましょう。
func (f *FactorDB) Connect() error { values := url.Values{} values.Add("query", fmt.Sprintf("%d", f.Number)) resp, err := http.Get(fmt.Sprintf("%s?%s", ENDPOINT, values.Encode())) if err != nil { return errors.New("cannot connect" + ENDPOINT) } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { return errors.New("empty body") } response := handler.ConvertToFactorDB(b) f.Result = response return nil }
出ました。これが ポインターレシーバーですねw こうするオブジェクト指向プログラミングで言うところのメソッドのように扱えます! あとはmain 関数等で以下のような感じで呼び出せば OK です。
if err := factordb.Connect(); err != nil { fmt.Println(err) }
また、Pythonのメソッドでいうところの
- ID を取得する:
get_id
- Status :
get_status
get_factor_list
に相当するポインターレシーバー関数を定義します。ちなみに最後の get_factor_list
はAPIから返ってくるふざけた形式をいい感じに返してくれるメソッドで以下のようなコードで書かれています。
def get_factor_list(self): """ get_factors: [['2', 3], ['3', 2]] Returns: [2, 2, 2, 3, 3] """ factors = self.get_factor_from_api() if not factors: return [] ml = [[int(x)] * y for x, y in factors] return [y for x in ml for y in x]
リスト内包表記をバリバリ使っているので読みづらいですが動きとしては書いてあるとおりです。
Goでは次のように書きました。
func (f *FactorDB) GetFactorList() ([]int, error) { if f.Empty() { return []int{}, errors.New("Empty Result") } var ret []int for _, f := range f.Result.Factors { for i := 0; i < f.Power; i++ { ret = append(ret, f.Number) } } return ret, nil }
うーんめっちゃ愚直w
テストコードとこれらのメソッドのDiff差分は以下です。 https://github.com/ryosan-470/factordb-go/commit/1657c0397571762061f1c7112c512bd11498c99d
CLIの完成
さて、これら3つを組み合わせてCLIを作成します。動作としては単純で第1引数の値を受取り、FactorDB
構造体の Number
に代入し、FactorDB.Connect
を呼び、あとはよしなに各関数で値を整形して標準出力に表示するというものです。
強いて言えば JSON 形式で返すときに物凄く無理やりArrayからJSON文字列を書き出しているところが少し気持ち悪いです。
func callAction(c *cli.Context) error { var number = "" if len(c.Args()) > 0 { number = c.Args().First() } n, err := strconv.Atoi(number) if err != nil { log.Fatal("Your input is not number") } f := factordb.FactorDB{Number: n} if err := f.Connect(); err != nil { log.Fatal("Connection Error") } factors, _ := f.GetFactorList() var output string var isJson = c.GlobalBool("json") if isJson { id, _ := f.GetId() status, _ := f.GetStatus() var fs []string for _, f := range factors { fs = append(fs, fmt.Sprintf("%d", f)) } facs := fmt.Sprintf("[%s]", strings.Join(fs, ", ")) output = fmt.Sprintf("{\"id\": \"https://factordb.com/?id=%s\", \"status\": \"%s\", \"factors\": %v}", id, status, facs) } else { output = strings.Trim(fmt.Sprintf("%v", factors), "[]") } fmt.Printf("%s\n", output) return nil }
とりあえず、動くっぽいコードは以下のコミットハッシュでできました。
https://github.com/ryosan-470/factordb-go/commit/796c3a6828209561d17bda8813721355b74dfc5e
次に Ruby で言うところの Gemfile 的なもので外部パッケージのバージョンを管理します。Goではglideというものを使うとできるそうです。
https://github.com/ryosan-470/factordb-go/commit/ffeb93d6f289e3a35082c04aaaf574f762a0c1c6
CircleCI 2.0を使ってテストする
さて、ここまで来たら今度はCIを回しましょう。有名なCIサービスにはTravisやCircleCIがありますが最近はCircleCI 2.0の高速さに驚かされているため、CircleCIを採用しました。CircleCI 2.0とはなんぞやという人は私が過去に書いた記事を読んでみてください。 ビルドにVMを利用せずDockerコンテナがそのまま扱えるため、ビルド開始も高速に行われ非常に便利です!
config.yml は以下のとおりです。
version: 2 jobs: build: docker: - image: circleci/golang:1.8 # CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/ working_directory: /go/src/github.com/ryosan-470/factordb-go steps: - checkout - run: curl https://glide.sh/get | sh - run: make deps - run: make - run: name: Running test code command: make test
さりげなくビルドはすべて Makefile
に書いてありますが make test
の挙動としては
go test -v $(go list /... | grep -v /vendor/)
となっています。これにより、Glideで入ってくる外部ライブラリをテストせず自分のコードのみをテストすることが可能になっています。
テストカバレッジの計測
今回はテストカバレッジの計測とビジュアル化のために、Codecov を使いました。
Goでcoverageを測定するには難癖があるみたいでいろいろ調べてでてきたものを使用しています。
https://github.com/ryosan-470/factordb-go/commit/25e3dc1277678e883560191b7944285eab58da67
記事執筆の段階でカバレッジ率が59%でGo lintなどの測定もはいっていないのでそれらも入れていきたいなと思っているところです。
Windowsで動作を保証するためにAppveyorを使う
ここまで来たからにはWindowsでビルドし動くことも確かめたいなと思いました。WindowsでCIを行えるサービスのうち有名なものにAppveyorがあります。はじめて使ってみましたがそこまで難しくなかったのでその情報も共有しておきます。
特にいじらなければ、Windows Server 2012上でテストが行われるようです。
appveyor.yml
version: 1.0.{build} platform: x64 clone_folder: c:\gopath\src\github.com\ryosan-470\factordb-go environment: GOPATH: c:\gopath install: - set PATH=C:\msys64\mingw64\bin;%PATH% - echo %PATH% - echo %GOPATH% - go version - go env - go get -v -t -d ./... build_script: - go build -o factordb.exe - go test -v -race ./... artifacts: - path: factordb.exe name: binary
そんなに難しいことはしていないですね。ただ、Makefileが使えないのでその部分だけ手書きしたという感じでしょうか。一応テストも行っているのでもしWindowsではビルドできないみたいな状況が起きてもテストで落ちてくれると思います。
WindowsのCIの結果:
Python版のCLIでもWindowsで動くことには動きますが、Windowsには元々Pythonはいっていませんし、文字コードの問題で動かなかったりといろいろあるのでバイナリが各環境で吐けるGoはいいですね!
ここまでで役に立った資料、手法など
基本的に1次情報はGoの公式ドキュメントで、あそこを読めば大体のことはわかります。ただ、如何せん初心者なので意味を汲み取れないなどのときに適当に検索していると大抵同じようなものにぶつかった人がいるのでそこから情報を得ました。
fmt モジュールが便利
fmtモジュールでは、値をただ出力するだけでなくその値の型や値の中身を表示することができます。
fmt - The Go Programming Language
一番上に書いてあるとおり、
%v
: デフォルトフォーマットで値を表示する。構造体を表示するときは%+v
を使う%#v
: 値のGo言語構文表現%T
: 値の型のGo言語構文表現
これらを使っていい感じにデバッグしていました。型エラーが大抵多い気がするのでこれでprintfデバッグして確認しましょう。
gore を使うと REPL が動かせる
毎度毎度ちょっとしたコマンドの確認にコンパイルして実行は面倒くさいのでREPLがあるとはかどります。
疑問
- Packagingの切り方がよくわからない
- Go routineを使う場所がなかったのでその辺を勉強したい
まとめ
GoはPythonから移行してもそこまで難しくなく簡単に書くことができました。また、Windows用のバイナリをはけるなどLinuxやMacで日々開発している人にとって他のディストリビューション用にも開発できるのは非常にありがたいです。
もし、興味を持ってくれたら、このリポジトリを Star してね♡
今度はSlackのBOTをGoで書き直してみようと思っています。