goの型をdefined typeとcomposite typeに2分出来ない理由
go1.18から型システムに大きな変更が加えられるという事で、改めてgoの型について学習しています。
教材としてGo言語仕様書輪読会
さんの発表スライドを大変参考にさせて頂いているのですが、defined type
の項目で気になる文言が
defined typeではない型は、全て型リテラルで表される型である
goの型は主に
- 事前定義型
- ユーザー定義型
- コンポジット型
の3要素で構成されていると認識していて、defined type
が事前定義型+ユーザー定義型を表すのなら、それ以外全てはコンポジット型なのでは?と考えていました。
その為「型リテラルで表される型である」という表現に若干違和感を覚えたのですが、以下のQAセクションでその理由が言及されていて
defined typeでないtypeはcomposite typeと呼べばいいんでしょうか?
ちょっと明確に答えられないところです。 composite typeという語は仕様書に現れるのですが明確に定義されていません。 string のような型リテラルがcomposite typeであることは確かなのですが、 type S string のようにして定義したSがcomposite typeなのかどうかはっきりと言えないためです。 defined typeではない型に対す
確かに、、、
この場合ユーザー定義型Sはdefined typeとコンポジット型、二つの性質を持つ事になってしまうので、defined typeとcomposite typeを完全に別物と定義するにはcomposite typeが「ユーザー定義型には適応されない」という文言が必要そうです。
なるほど、、、
goでheapを実装する
heapとは優先度付きキューの一種。親node > 子node
又は親node < 子node
な制約を持った木構造です。
出典 wikipedia
画像で見ると分かり易いですね。配列の中で一番小さい値がroot node。そこから深くなる毎に値が大きくなっています。左右の子ノード同士に大小関係はありません。
似たデータ構造に二分探索木がありますが、あちらは「左の子孫ノードの値 < 親ノードの値 < 右の子孫ノードの値」な制約をもった木構造です。
出典 wikipedia
使い分けとしては
- root node以外の特定のnodeを検索したい場合に効果は二分探索木
- treeの中の最大 or 最小値を知りたい時はヒープ
標準ライブラリcontainer/heap
goは標準ライブラリcontainer/heapでheapの実装をサポートしています。
// IntHeap は,整数の最小ヒープです。 type IntHeap []int func (h IntHeap) Len() int { return len(h) } func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } func (h *IntHeap) Push(x interface{}) { // Push と Pop はポインタレシーバを使っています。 // なぜなら,スライスの中身だけでなく,スライスの長さも変更するからです。 *h = append(*h, x.(int)) } func (h *IntHeap) Pop() interface{} { old := *h n := len(old) x := old[n-1] *h = old[0 : n-1] return x } func Example_intHeap() { h := &IntHeap{2, 1, 5} heap.Init(h) heap.Push(h, 3) fmt.Printf("minimum: %d\n", (*h)[0]) for h.Len() > 0 { fmt.Printf("%d ", heap.Pop(h)) } // Output: // minimum: 1 // 1 2 3 5 }
こちらはGoDoc
から拝借したサンプルコードです。
この様にLen Less Swap Push Pop
5つのメソッドを定義したHeap
構造体を作成し、初期化メソッドheap.Init
の引数として渡す事でroot nodeを最小としたheap木を作成してくれます。
heap.Push
,heap.Pop
は一見配列の要素を1つ増減させるだけのメソッドに見えますが、内部では配列内を走査し、親node < 子node
の制約の元indexを並べ変えています。
標準ライブラリを使った実装パターンが分かった所で少し深堀りしていきましょう。
深堀
この動画を見てください!
FIN
許してください。手抜きではないです。どんなに文字を尽してもこの動画よりわかりやすい解説はできないんです、、、
アルゴリズムの様に動的にデータが移動する概念に関しては動画を見た方が絶対理解が捗ると思います。この方の動画全部面白いので他のも見て行ってください、、、
この後は蛇足ではありますが動画内の実装内容と標準ライブラリのインターフェースが若干解離している為、文面での解説を加えながら少し解説します
Push と Popの内部処理
先ほども言及しましたがheap.Push
とheap.Pop
は単なる要素増減以外にも、配列内の整合性を保つ為に「並び替え」を行っています。それが動画内のmaxHeapifyUp
, maxHeapifyDown
に当たる処理です。
container/heap
ライブラリでは最小の値をrootとしている為メソッド名はminHeapifyUp
, minHeapifyDown
とした方が自然でしょうか
まずはPush
の挙動から見ていきましょう。
type Heap []int func (h *Heap) Push(key int) { // 値を追加 *h = append(*h, key) // 最後尾のindexを指定 h.minHeapifyUp(len(*h)-1) } func (h *Heap) minHeapifyUp(index int) { // 親nodeより小さければ for (*h)[parent(index)] > (*h)[index] { h.swap(parent(index), index) index = parent(index) } } func parent(i int) int { return (i-1)/2 } func (h *Heap) swap(i1, i2 int) { (*h)[i1], (*h)[i2] = (*h)[i2], (*h)[i1] }
サンプルのPush
メソッドに1行加えました。
minHeapifyUp
は新しく加えられた値が親の値より小さければその位置をswap
し続けるメソッドです。
例えば新しく加えられた値がその配列の最小要素だった場合、root nodeに達するまでswapし続けます。
parent
関数は親nodeのインデックスを算出する為の関数です。treeにおけるindex番号の親子関係は左の子node = 親node * 2 + 1
、右の子node = 親node * 2 + 2
という式で表せる為、親indexも親node = (子node-1) / 2
で割り出せます。
たとえばindex1の値の子要素を調べたい場合。
図が大雑把で申し訳ありませんが、丸の中が値、左がindex番号です。
この様に1の子要素二つのindexは12+1の3と12+2の4であることがわかると思います。
次にPop
の処理は少し複雑
func (h *Heap) Pop() int { if len(*h) == 0 { fmt.Println("cannot extract beacause array length is 0") return -1 } // 除外されるroot node extracted := (*h)[0] l := len(*h)-1 // 最後尾の値をroot nodeにすげ替える (*h)[0] = (*h)[l] // 最後尾の値を切り捨て (*h) = (*h)[:l] // root nodeと子nodeを比較。root node < 子nodeの小さい方ならswap h.minHeapifyDown(0) return extracted } func (h *Heap) minHeapifyDown(index int) { lastIndex := len(*h) -1 l, r := left(index), right(index) childToCompare := 0 // 左辺しか存在しない場合 for l <= lastIndex { if l == lastIndex { childToCompare = l } else if (*h)[l] < (*h)[r] { childToCompare = l } else { childToCompare =r } if (*h)[index] > (*h)[childToCompare] { h.swap(index, childToCompare) index = childToCompare l, r = left(index), right(index) } else { return } } } func left(i int) int { return i*2+1 } func right(i int) int { return i*2+2 }
Pop関数の挙動は大まかに2つ。配列内の最小値(root node)を排除して排除した値を返す事です。
ポイントはroot nodeを排除した後もheap木の制約を守り続ける点。minHeapifyDown
で都度ソートしなおします
具体的なメソッドの挙動としては
- 頂点の値(root node)を最後尾の値にすげ替える
- 最後尾の値を削除
- 新しく代入されたroot nodeと二つの子nodeの内小さい方の値を比較。root nodeの方が大きい場合swapを実行
- これを繰り返す
こうして親node < 子nodeの制約を守り続けます。
例題 D - Prefix K-th Max
最後に演習問題を一つ
(1,2,…,N) の順列 P=(P1,P2,…,PN)、および正整数 K が与えられます。 i=K,K+1,…,N について、以下を求めてください。 P の先頭 i 項のうち、K 番目に大きい値
簡単に要約すると
- 配列の中から要素数Kのスライスを切り出し、その中から最小の値を出力
- これを
K,K+1,…,N
まで繰り返す
解答
root nodeを最小値としたheap treeを構成します。
func main() { var n, k int var pp []int h := &Heap{} for _, v := range pp[:k-1] { h.Push(v) } now := 0 for i := k-1; i < n; i++ { cur := pp[i] if cur > now { h.Push(cur) now = h.Pop() } fmt.Println(, now) } } type Heap []int func (h *Heap) Push(key int) { *h = append(*h, key) h.minHeapifyUp(len(*h)-1) } func (h *Heap) Pop() int { if len(*h) == 0 { fmt.Println("cannot extract beacause array length is 0") return -1 } extracted := (*h)[0] l := len(*h)-1 (*h)[0] = (*h)[l] (*h) = (*h)[:l] h.minHeapifyDown(0) return extracted } func (h *Heap) minHeapifyUp(index int) { for (*h)[parent(index)] > (*h)[index] { h.swap(parent(index), index) index = parent(index) } } func (h *Heap) minHeapifyDown(index int) { lastIndex := len(*h) -1 l, r := left(index), right(index) childToCompare := 0 // 左辺しか存在しない場合 for l <= lastIndex { if l == lastIndex { childToCompare = l } else if (*h)[l] < (*h)[r] { childToCompare = l } else { childToCompare =r } if (*h)[index] > (*h)[childToCompare] { h.swap(index, childToCompare) index = childToCompare l, r = left(index), right(index) } else { return } } } func parent(i int) int { return (i-1)/2 } func left(i int) int { return i*2+1 } func right(i int) int { return i*2+2 } func (h *Heap) swap(i1, i2 int) { (*h)[i1], (*h)[i2] = (*h)[i2], (*h)[i1] }
見づらいので入出力のバッファリングは省略しています。
正直標準パッケージでも全然問題ありません!が、実際に実装してみるとやっぱり頭に入りますね(reflect
パッケージを使わない分実行速度の面でも多少優れています)
【Go】AtCoderでよく使う関数
はじめに
goでAt Coderを解くにあたってC++で書かれた解説コードをGoに焼き直す作業を頻繁に行っています。その際「C++に有ってgoにはない」メソッドや文法が多くて結構困ります
冗長なだけなら「goだし仕方ないか」で済むのですが、明らかにあった方が良いメソッドが標準パッケージに搭載されていない事もしばしば
そこで幾つか自分で実装してみよう!
配列操作
C++のvectorやiteratorで使われる関数をもうちょい足したい
count
func Count(s []int, n int) int { var cnt int for _, v := range s { if v == n { cnt++ } } return cnt }
unique
func Unique(s []int) []int { uniq := make([]int, len(s)) m := make(map[int]bool) cnt := 0 for _, ele := range s { if !m[ele] { m[ele] = true uniq[cnt] = ele cnt++ } } return uniq[:cnt] }
文字列に対する関数
find
第1引数の集合に第2引数の値が存在した場合、そのindexを返す関数
引数にinterface{}
型を取り、reflectで型判別しながら異なる処理を呼び出すことで実装できますが、実行速度が大幅に遅くなる為文字列に絞って実装します。
可変長配列ならソートしてからsort.Search
で二分探索すれば良いはず
func find(str string, char string) int { for i := range str { if string(str[i]) == char { return i } } return -1 }
O(N)
汎用計算関数
std::abs
std::max
std::min
std::pow
std::sqrt
func max(x, y int) int { if x > y { return x } return y } func min(x, y int) int{ if x > y { return y } return x } func abs(a int) int { return int(math.Abs(float64(a))) } func pow(p, q int) int { return int(math.Pow(float64(p), float64(q))) } func squart(x, y int) float64 { dif := x*x - y*y return math.Sqrt(float64(dif)) } func pow(x, y int) int { return int(math.Pow(float64(x), float64(y))) // 2, 3 = 8 // 10, 2 = 100
permulation
順列を扱う際にお世話になるメソッドです
func Factorial(n int)(result int) { if (n > 0) { result = n * Factorial(n-1) return result } return 1 } /* fmt.Println(0, intSlice) for i := 1; nextPermutation(sort.IntSlice(intSlice)); i++ { fmt.Println(i, intSlice) } */ func nextPermutation(x sort.Interface) bool { n := x.Len() - 1 if n < 1 { return false } j := n - 1 for ; !x.Less(j, j+1); j-- { if j == 0 { return false } } l := n for !x.Less(j, l) { l-- } x.Swap(j, l) for k, l := j+1, n; k < l; { x.Swap(k, l) k++ l-- } return true }
Factorialは日本語で「階乗」の意味でN!を算出します。(3! は 3、10! は 45)
次にnextPermulationはC++のnextPermulation
と同じ挙動です。順列の全パターンを出力し、最大値まで並べ終えるとfalseを返します
最後に
同じ様なコンセプトで「標準パッケージにないけど欲しい」関数があれば是非教えていただきたいです!
c++ちゃんと勉強したい
ssh接続を少し簡単にするcli
https://github.com/dasuken/sshgengithub.com
ssh接続の度に.ssh/config
ファイル編集するの面倒だなーと思って作りました
使い方
go get github.com/dasuken/sshgen sshgen add
あとはinteractiveに必要な情報を入力していただければ設定ファイルに書き込みます
鍵のpathはデフォルトで環境変数$IDENTITY_FILE
の値を読み込む仕様なので、毎度の入力が面倒な場合はexportしていただきたいです!
マイルストーン
- update
- delete
- list
rune型は単なるint32のエイリアス
runeの疑問
rangeでrune型を取り出す際
func main() { str := "A" var x rune for i, v := range str { x = v x = str[i] // 代入出来ない } }
値はrune型。けどインデックスアクセスで取得した値はrune型じゃない。
どちらもstring()
でキャストしたらAなのに何で?
型を調べる
func main() { str := "A" for i, v := range str { fmt.Printf("str[i] value: %v, type: %T \n" , str[i], str[i]) fmt.Printf("v value: %v, type: %T" , v, v) } }
結果
str[i] value: 65, type: uint8 v value: 65, type: int32
値はどちらも65
しかし型は相違。インデックスアクセスの場合はuint8
, vの場合はint32
実はこれらはbyte
とrune
のエイリアスになっていて、
var x rune = int32(1) var y byte = uint8(1)
この様に書いてもコンパイルエラーにならない。
''
シングルクオートで囲った文字列もrune型として出力
'A' // 65
rangeの値と一緒。どちらもint32
の65を示す
func main() { str := "A" for _, v := range str { if v == 'A' {} // true } }
逆に言うと、int32(65)
をstringでcastするとA
という文字列を取得できる
fmt.Println(string(int32(65))) // A
個人的には結構衝撃的だった
アルファベットを出力する問題
runeとstirngを使った良い例題
英大文字のみからなる文字列 S があります。また、整数 N が与えられます。 S の各文字を、アルファベット順で N 個後の文字に置き換えた文字列を出力してください。 ただしアルファベット順で Z の 1 個後の文字は A とみなします。
直感的にとくと
str := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" m := map[string]int{} for i, v := range str { m[string(v)] = i } var ans string for _, v := range s { i := m[string(v)] target := (i+n) % 26 ans += string(str[target]) } fmt.Println(ans)
値とindexを反転させたmapを作って、各値に対応したindexにnを足した値をSから求める
しかしruneを使えばもっとシンプルに解ける
func main() { scanner := makeScanner(16384) n := eGetInt(scanner) s := eGetLine(scanner) rs := make([]byte, len(s)) alpha := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" for i, c := range s { order := c - 'A' slide := (int(order) + n) % 26 rs[i] = alpha[slide] } fmt.Println(string(rs)) }
アルファベットのruneは連番になっている為、対象文字列をrangeで回して値-Aのrune値
を算出すれば、それがアルファベットの何番目の文字かint32
型で取得できる。
この時取得した値にNを足し、26で割ったあまりが欲しいアルファベットのインデックス。alpha[index番号]
で得られる値はint32
ではなくuint8
なのでbyte配列に結果を代入し最後にキャストする
まとめ
【Go】実行速度と汎用性を両立させたContains関数作ってみた
きっかけ
Goの可変長配列であるスライス。配列の実態を考慮する必要がないシンプルさが売りです。
が、シンプルすぎるが故欲しい関数が実装されていない事もしばしば。特に特定の値が存在するか判定するContains
関数などは使いたい場面が多いです。
そこで今回は出来る限り汎用性と実行速度を両立させたContains関数の実装を目指します。
書いてみる
まずはシンプルに実装します
func main() { Contains([]int{1, 2, 3, 4, 5}, 3) } func Contains(s []int, n int) bool { for _, v := range s { if v == n { return true } } return false }
動く!けどint
のスライス型しか引数にとれませんね
reflect
で引数に汎用性を持たせる
引数に汎用性を持たせる為によく使う手法がreflect
パッケージによる抽象化です。
reflect
パッケージとはinterface{}
型の変数から型と値情報を検出するパッケージ。
調べるとGoで型に縛られない良い感じのContains関数の書き方に実装例が紹介されていました。やっぱり誰か試してるよね。
structだろうがfloatだろうがpanic起こさず判別出来ます。
問題点
reflectパッケージの欠点として実行速度が遅さが挙げられます。コンパイル時点で型定義できないのでアロケーション回数が増えてしまう様ですね。
一体どの程度実行速度に影響が生まれるのでしょうか?
ベンチマーク
実際に二つの実行速度を計測します
ContainsSim
reflectなしContainsCom
reflectあり
結果
BenchmarkContainsSim BenchmarkContainsSim-4 85581004 12.01 ns/op BenchmarkContainsCom BenchmarkContainsCom-4 870052 1243 ns/op
うーむ。。。100倍近い差が生まれています
改善
いくらなんでもパフォーマンスに差があり過ぎるので、少し改善を測ってみます。
ボトルネック調査
41.3% reflect.DeepEqual 25.8% reflect.Value.Interface 5.6% reflect.Value.Index
プロファイル結果の一部抜粋。様はDeepEqualとInterfaceがボトルネックっぽいですね。
改善案1
DeepEqual
を抜きたい- 引数の型を絞る
- 構造体等は諦めて、stringやint,floatのみ
Interface()
を抜きたい- 調べてみると
reflect.Type
の構造体をInterface{}
型の変数に戻すだけのメソッド DeepEqual
を使わないのであればinterface{}
型の変数は必要ないはず
- 調べてみると
結局Contains
関数の第一引数に入るのはほとんどの場合[]int []string []float
のどれかだと思うので、構造体は諦めて実装
func ContainsMid(list interface{}, elem interface{}) bool { listV := reflect.ValueOf(list) if listV.Kind() == reflect.Slice { for i := 0; i < listV.Len(); i++ { item := listV.Index(i) if !reflect.TypeOf(elem).ConvertibleTo(item.Type()) { return false } switch elem.(type) { case string: if elem == item.String() { return true } case int: if elem == int(item.Int()) { return true } case int64: if elem == item.Int() { return true } case int32: if elem == int32(item.Int()) { return true } case int16: if elem == int16(item.Int()) { return true } case int8: if elem == int8(item.Int()) { return true } case float64: if elem == float64(item.Int()) { return true } case float32: if elem == float32(item.Int()) { return true } default: panic(errors.New("contains() cannot take a type other than string and int as an argument")) } } } return false }
string
, int8-64
, float
以外の型を与えるとpanic
が出る設計。err返しちゃった方が良いのかな
結果
BenchmarkContainsMid BenchmarkContainsMid-4 4004848 302.7 ns/op
3倍速にはなりましたがまだ遅いですね
プロファイル結果をみるとConvertible
位は削れそう
24.5% reflect.Value.Index() 19.6% reflect.Convertible()
加えて引数にintのスライスとstringを与えるとバグが発生
func main() { Contains([]int{1, 2, 3}, "a") // panic }
Convertible()
というメソッド名からint
とstring
の差分を検出してくれそうな気がしていたのですが機能していない様子。何するメソッドなんやこれ。doc読んでも良くわかりませんでした
改善案2
- バグの原因+ボトルネックな
reflect.Convertible
を取り除く
func ContainsMid(list interface{}, elem interface{}) bool { listV := reflect.ValueOf(list) if listV.Kind() == reflect.Slice { for i := 0; i < listV.Len(); i++ { item := listV.Index(i) switch elem.(type) { case string: if item.Kind() == reflect.String && elem == item.String() { return true } case int: if item.Kind() == reflect.Int && elem == int(item.Int()) { return true } case int64: if item.Kind() == reflect.Int64 && elem == item.Int() { return true } case int32: if item.Kind() == reflect.Int32 && elem == int32(item.Int()) { return true } case int16: if item.Kind() == reflect.Int16 && elem == int16(item.Int()) { return true } case int8: if item.Kind() == reflect.Int8 && elem == int8(item.Int()) { return true } case float64: if item.Kind() == reflect.Float64 && elem == float64(item.Int()) { return true } case float32: if item.Kind() == reflect.Float32 && elem == float32(item.Int()) { return true } default: panic(errors.New("contains() cannot take a type other than string and int as an argument")) } } } return false }
結果
BenchmarkContainsMid BenchmarkContainsMid-4 7484228 157.6 ns/op PASS
更に倍速になりました。
結論
- 引数をstring, int, float型に限定する事でパフォーマンスは10倍程度あがった
- 汎用性を削った結果予期しない引数を与えるとpanicが発生
器用貧乏。。。?引数の汎用性を落とすなら、素直にプリミティブ型それぞれにContainsXXX()
といった形でメソッド作った方がいい気もします。
けど折角作ったので暫くideの予測変換に追加して使ってみようと思います
参考
【Golang】urfave/cliを使って計算機アプリをcliにする
Urfave
CLIを実装する上で面倒な
- サブコマンド
- リッチなヘルプインターフェース
等の作成をサポートしてくれます。
今回の記事では基本的なメソッドや構造体を説明した後、演習として実際に計算機をcli化させる過程を紹介します。
なお、演習部分は以下の動画の翻訳兼まとめです。是非元動画もご参照ください。
ep008 Using 'urfave-cli/v2' Package
インストール
go get -u github.com/urfave/cli/v2
使い方
func main() { // 構造体作成 app := cli.NewApp() app.Name = "mycalc" app.Usage = "The best calculator" app.Description = "this is calculator<ffff>" // 実行したい関数 app.Action = mainAction // フラグ --引数 値 app.Flags = []cli.Flag{ &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, } // command app.Commands = []*cli.Command{ *cli.Command { return &cli.Command{ Name: "add", Aliases: []string{"a"}, Action: func(ctx *cli.Context) error { res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) return nil }, } } app.Run(os.Args) }
実行結果
// app.Name - app.Usage NAME: mycalc - The bes実行t calculator USAGE: main [global options] command [command options] [arguments...] // app.Description DESCRIPTION: this is calculator<ffff> // app.Command COMMANDS: add, a help, h Shows a list of commands or help for one command // app.Flags GLOBAL OPTIONS: --oper value add, sub, mul or div operations on two int (default: "add") --opt1 value first int (default: 0) --opt2 value second int (default: 0) --help, -h show help (default: false)
各構造体やフィールドの役割は以下です
cli.App
- 元になる構造体。このメンバ変数に様々な値を代入します。
Name
,Usage
,Description
- ヘルプに表示したい情報を付与
Action
- 実行したい関数を定義
Flags
- Actionで実行する際、使いたいflag引数を定義。helpで参照可能
Commands
cli.Command
ポインタ型のスライスを渡す事でサブコマンドを実装cli.Command
構造体にもcli.App
と同様にdescriptionやActionを定義していきます。小さいcli.App
を沢山作っていく感覚です。
Run()
- 実行メソッド
アプリ解説
これらを踏まえて簡単な計算機アプリを段階的にCLI化させていきます。
今回使う計算機アプリです
var ( oper string op1 int op2 int ) func main() { flag.StringVar(&oper, "oper", "add", "add, sub, mul or div operations on two int") flag.IntVar(&opt1, "opt1", 0, "first int") flag.IntVar(&opt2, "opt2", 0, "second int") flag.Parse() switch oper { case "add": res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) case "mul": res := opt1 * opt2 fmt.Printf("%v * %v = %v", opt1, opt2, res) } }
$ go run main.go --opt1 1 --opt2 2 --oper mul 1 * 2 = 2 $ go run main.go --opt1 1 --opt2 2 --oper add 1 + 2 = 3
現状
- 二つの数字とオペレーションを渡す
- スイッチ文で処理を変える
これだけ。しかしcliというからには
- -hオプションで分かりやすいヘルプを表示する
- オペレーションはオプションではなくサブコマンドで行う
- 引数は2つだけではなく複数可能
位の機能は欲しい!
1. ヘルプを整える
まずアプリケーションの情報を付け加えます
var ( oper string op1 int op2 int ) func main() { app := cli.NewApp() app.Name = "mycalc" app.Usage = "The best calculator" app.Description = "this is calculator<ffff>" app.Action = mainAction app.Run(os.Args) } func mainAction(ctx * cli.Context) error { flag.StringVar(&oper, "oper", "add", "add, sub, mul or div operations on two int") flag.IntVar(&opt1, "opt1", 0, "first int") flag.IntVar(&opt2, "opt2", 0, "second int") flag.Parse() switch oper { case "add": res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) case "mul": res := opt1 * opt2 fmt.Printf("%v * %v = %v", opt1, opt2, res) } return nil }
変更点
- main関数でcli.app構造体を作成。各種情報を渡す
- 既存のアプリケーションを
app.Action
の引数として渡せる様引数と返り値を調整
結果
$ go run main.go -h NAME: mycalc - The best calculator USAGE: main [global options] command [command options] [arguments...] DESCRIPTION: this is calculator COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false)
そこそこリッチなヘルプが表示される様になりました。
2. オプションを表示
ヘルプにフラグ引数の案内は欲しい所ですよね(GLOBAL OPTIONS以下
)
そこでmainAction内のフラグ引数をappオブジェクトに移動します
func main() { // 追加 // destinationに変数のポインタ。それ以外はフラグと同じ app.Flags = []cli.Flag{ &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, } } func mainAction(ctx * cli.Context) error { // フラグを消す switch oper { case "add": res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) case "mul": res := opt1 * opt2 fmt.Printf("%v * %v = %v", opt1, opt2, res) } return nil }
これでGLOBAL OPTIONSに案内が表示される様になりました
$ go run main.go -h NAME: mycalc - The best calculator ... GLOBAL OPTIONS: --oper value add, sub, mul or div operations on two int (default: "add") --opt1 value first int (default: 0) --opt2 value second int (default: 0) --help, -h show help (default: false)
一度挙動も確認してみましょう
$ go run --op1 1 --op2 2 --oper add 1 + 2 = 3
問題なく動くはずです。
しかし--oper
が引数になってる点は違和感です。oper
はアプリケーションの動作自体を変えている為引数ではなく
go run add --op1 1 --op2 2
の様にコマンドにした方が分かり安いcliインターフェースであると言えそうです
3. operをサブコマンドに
func main() { ... app.Commands = []*cli.Command{ addCommand(), mulCommand(), } app.Run(os.Args) } func addCommand() *cli.Command { return &cli.Command{ Name: "add", Aliases: []string{"a"}, Action: func(ctx *cli.Context) error { res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) return nil }, } } func mulCommand() *cli.Command { return &cli.Command { Name: "mul", Aliases: []string{"m"}, Action: func(ctx *cli.Context) error { res := opt1 * opt2 fmt.Printf("%v * %v = %v", opt1, opt2, res) return nil }, } } func mainAction(ctx * cli.Context) error { // スイッチ文消す return nil }
app.Commands
に*cli.Command
のスライスを渡す事でサブコマンドが実装出来ます。
cli.Command
構造体はcli.App
構造体と同じ様に個別のコマンドやヘルプ、オプションを持つ事が出来るので、コマンドを複製するイメージで使う事ができます。
又、動画ではネストが深くならない様にaddCommand()
関数の返り値を渡しています。
$ go run main.go -h USAGE: main [global options] command [command options] [arguments...] COMMANDS: add, a mul, m help, h Shows a list of commands or help for one command
これでサブコマンドが実装出来ました。
Usageに従っていくつか実行して見ましょう
$ go run main.go --opt1 6 add 6 + 0 = 6% $ go run main.go --opt1 3 --opt2 4 a 3 + 4 = 7% $ go run main.go --opt1 6 --opt2 1 mul 6 * 1 = 6%
ちゃんとエイリアスも機能しています。
が、引数の順番がおかしいです。引数より先にサブコマンドを指定する方が自然ですよね。
$ go run main.go mul --opt1 6 --opt2 1
Urfave
ではUsageの通りglobal options
command
の順で実行しなければならない為command
の後に引数を実行したい場合はglobal options
の引数をcli.Command
以下に移動させる必要があります。
4. optionsの移動
func main() { app := cli.NewApp() app.Name = "mycalc" app.Usage = "The best calculator" app.Description = "this is calculator<ffff>" app.Action = mainAction //app.Flags = []cli.Flag{ // &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, // &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, // &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, //} app.Commands = []*cli.Command{ addCommand(), mulCommand(), } app.Run(os.Args) } func addCommand() *cli.Command { return &cli.Command{ Name: "add", Aliases: []string{"a"}, Action: func(ctx *cli.Context) error { res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) return nil }, Flags: []cli.Flag{ &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, }, } } func mulCommand() *cli.Command { return &cli.Command { Name: "mul", Aliases: []string{"m"}, Action: func(ctx *cli.Context) error { res := opt1 * opt2 fmt.Printf("%v * %v = %v", opt1, opt2, res) return nil }, Flags: []cli.Flag{ &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, }, } }
$ go run main.go add --opt1 1 --opt2 2 1 + 2 = 3
引数の順番が自然になりました。
又、ヘルプを見るとGlobal Optionsからフラグ引数の案内が消え、サブコマンドのヘルプに表示されるようになっています。
$ go run main.go add -h NAME: main add - USAGE: main add [command options] [arguments...] OPTIONS: --oper value add, sub, mul or div operations on two int (default: "add") --opt1 value first int (default: 0) --opt2 value second int (default: 0) --help, -h show help (default: false)
5. フラグ引数を省く
これでフラグ引数をサブコマンドの後に実行できる様になりました。
が、果たして計算機アプリにフラグ引数は必要なのでしょうか?態々opt1 opt2
の様に必要な引数を説明せずともuserは分かってくれるはずです。寧ろ--optn
と打ち込む手間の方が煩わしいと思います。
又、計算機アプリなのに与えられる引数の個数が限定されているのも宜しくありません。
go run main.go add 1 2 3 1 + 2 + 3 = 6
この様に実行できるのが理想ではないでしょうか
そこで引数の受け取り方を変えてみます。
func addCommand() *cli.Command { return &cli.Command{ Name: "add", Aliases: []string{"a"}, Action: func(ctx *cli.Context) error { // 引数の数 n := ctx.NArg() if n == 0 { return fmt.Errorf("no argument provided for add operation") } a := ctx.Args().Get(0) res, _ := strconv.Atoi(a) // 一つ目の値をprint fmt.Print(res) // 二つ以上引数があった場合、順繰りにprint for i := 1; i < n; i++ { a := ctx.Args().Get(i) op, _ := strconv.Atoi(a) res += op fmt.Printf(" + %v", op) } // 結果 fmt.Printf(" = %v", res) return nil }, //Flags: []cli.Flag{ // &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, // &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, // &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, //}, } }
cli.Flagを全て消し、代わりにctx.Args()メソッドで引数を一つづつ取り出す方式に変更しました。
実行してみます
$ go run main.go add 1 2 1 + 2 = 3 $ go run main.go mul 1 2 3 -1 1 * 2 * 3 * -1 = -6 $ go run main.go a 1 4 10 -1 1 + 4 + 10 + -1 = 14
alias含め複数の引数を渡しても正常に動作している事が分かります。
最後に
引数が与えられなかった場合もヘルプが表示されるようにしましょう
package main import ( "fmt" "github.com/urfave/cli/v2" "os" "strconv" ) func main() { app := cli.NewApp() app.Name = "mycalc" app.Usage = "The best calculator" app.Description = "this is calculator<ffff>" app.Action = mainAction app.Commands = []*cli.Command{ addCommand(), mulCommand(), } app.Run(os.Args) } func addCommand() *cli.Command { return &cli.Command{ Name: "add", Aliases: []string{"a"}, Action: func(ctx *cli.Context) error { // 引数の数 n := ctx.NArg() if n == 0 { return fmt.Errorf("no argument provided for add operation") } a := ctx.Args().Get(0) res, _ := strconv.Atoi(a) fmt.Print(res) for i := 1; i < n; i++ { a := ctx.Args().Get(i) op, _ := strconv.Atoi(a) res += op fmt.Printf(" + %v", op) } fmt.Printf(" = %v", res) return nil }, } } func mulCommand() *cli.Command { return &cli.Command { Name: "mul", Aliases: []string{"m"}, Action: func(ctx *cli.Context) error { n := ctx.NArg() if n == 0 { return fmt.Errorf("no argument provided for add operation") } a := ctx.Args().Get(0) res, _ := strconv.Atoi(a) fmt.Print(res) for i := 1; i < n; i++ { a := ctx.Args().Get(i) op, _ := strconv.Atoi(a) res = res * op fmt.Printf(" + %v", op) } fmt.Printf(" = %v", res) return nil } } func mainAction(ctx * cli.Context) error { ctx.App.Command("help").Run(ctx) return nil }
まとめ
urfave
を使えば既存アプリを簡単にcli化する事ができます。
同系統のライブラリとしてcobra
も人気ですが、個人的にはurfave
のインターフェースの方がシンプルで好きです。