goの型をdefined typeとcomposite typeに2分出来ない理由

go1.18から型システムに大きな変更が加えられるという事で、改めてgoの型について学習しています。

教材としてGo言語仕様書輪読会さんの発表スライドを大変参考にさせて頂いているのですが、defined typeの項目で気になる文言が

defined types - Google スライド

defined typeではない型は、全て型リテラルで表される型である

goの型は主に

  • 事前定義型
  • ユーザー定義型
  • コンポジット型

の3要素で構成されていると認識していて、defined typeが事前定義型+ユーザー定義型を表すのなら、それ以外全てはコンポジット型なのでは?と考えていました。

その為「型リテラルで表される型である」という表現に若干違和感を覚えたのですが、以下のQAセクションでその理由が言及されていて

  1. defined typeでないtypeはcomposite typeと呼べばいいんでしょうか?

  2. ちょっと明確に答えられないところです。 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な制約を持った木構造です。

https://upload.wikimedia.org/wikipedia/commons/6/60/Binary_heap_indexing.png

出典 wikipedia

画像で見ると分かり易いですね。配列の中で一番小さい値がroot node。そこから深くなる毎に値が大きくなっています。左右の子ノード同士に大小関係はありません。

似たデータ構造に二分探索木がありますが、あちらは「左の子孫ノードの値 < 親ノードの値 < 右の子孫ノードの値」な制約をもった木構造です。

https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Binary_search_tree.svg/600px-Binary_search_tree.svg.png

出典 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 Pop5つのメソッドを定義したHeap構造体を作成し、初期化メソッドheap.Initの引数として渡す事でroot nodeを最小としたheap木を作成してくれます。

heap.Push,heap.Popは一見配列の要素を1つ増減させるだけのメソッドに見えますが、内部では配列内を走査し、親node < 子nodeの制約の元indexを並べ変えています。

標準ライブラリを使った実装パターンが分かった所で少し深堀りしていきましょう。

深堀

この動画を見てください!

www.youtube.com

FIN

許してください。手抜きではないです。どんなに文字を尽してもこの動画よりわかりやすい解説はできないんです、、、

アルゴリズムの様に動的にデータが移動する概念に関しては動画を見た方が絶対理解が捗ると思います。この方の動画全部面白いので他のも見て行ってください、、、

この後は蛇足ではありますが動画内の実装内容と標準ライブラリのインターフェースが若干解離している為、文面での解説を加えながら少し解説します

Push と Popの内部処理

先ほども言及しましたがheap.Pushheap.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で都度ソートしなおします

具体的なメソッドの挙動としては

  1. 頂点の値(root node)を最後尾の値にすげ替える
  2. 最後尾の値を削除
  3. 新しく代入されたroot nodeと二つの子nodeの内小さい方の値を比較。root nodeの方が大きい場合swapを実行
  4. これを繰り返す

こうして親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++vectoriteratorで使われる関数をもうちょい足したい

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

実はこれらはbyteruneエイリアスになっていて、

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配列に結果を代入し最後にキャストする

まとめ

  • byteとruneはuint8 int32のエイリアス
  • アルファベットのrune型は連番
  • stringをrangeで回した場合、得られる値はインデックスアクセスして取得した値と型が違う

【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()というメソッド名からintstringの差分を検出してくれそうな気がしていたのですが機能していない様子。何するメソッドなんやこれ。doc読んでも良くわかりませんでした

改善案2

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の予測変換に追加して使ってみようと思います

参考

Goにおけるreflect周り調べた

【Golang】urfave/cliを使って計算機アプリをcliにする

Urfave

golangcli作成ライブラリです。

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のインターフェースの方がシンプルで好きです。