Tambourine作業メモ

主にスキル習得のためにやった作業のメモ。他人には基本的に無用のものです。

GoでJSONをパースする

議事録のテンプレート的なことは、JSONで指定することにした。こんな感じ

{
      "会議名": "セッション0312",
      "開催日時": "2019年3月12日(火)14:00 – 17:00",
      "場所": "本社10F会議室",
      "出席者": {
        "ABC": ["安倍", "麻生", "石田", "山下", "河野"],
        "XYZ": ["柴山", "根本", "吉川", "世耕", "石井"]
      },
      "ToDo":[
        "概算見積の準備(XYZ)",
        "稟議書の準備(ABC)"
      ],
      "決定事項": [
        "検討の優先順位"
      ]
}

さて、これをGoで読み込んでみる。Rubyで読むと単にHashになって返ってくるだけなんだけども、Goのスタイルはこれに対応する構造体を作るらしい。 なるほど。

とはいうものの、とりあえずmapに読ませてみる。

import (
    "encoding/json"
    "fmt"
    "log"
)

func main() {
    data := []byte(
`{
  "会議名": "セッション0312",
  "開催日時": "2019年3月12日(火)14:00 – 17:00",
  "場所": "本社10F会議室"

}`)

    var m map[string]string
    err := json.Unmarshal(data, &m)
    if err != nil {
        log.Fatal(err)
    }

    for key, value := range m {
        fmt.Println(key, ":", value)
    }
}

これで単純にvalueがstringのところは読めるみたい。うん。便利じゃん?

とはいえ、これでは全体は読めないのでこんな感じにしてみる。

func main() {
    data := []byte(
        `{
  "会議名": "セッション0312",
  "開催日時": "2019年3月12日(火)14:00 – 17:00",
  "場所": "本社10F会議室",
  "出席者": {
      "ABC": ["安倍", "麻生", "石田", "山下", "河野"],
      "XYZ": ["柴山", "根本", "吉川", "世耕", "石井"]
  },
  "ToDo":[
      "概算見積の準備(XYZ)",
      "稟議書の準備(ABC)"
  ],
  "決定事項": [
      "検討の優先順位"
  ]
}`)

    type Meeting struct {
        Name      string              `json:"会議名"`
        Date      string              `json:"開催日時"`
        Room      string              `json:"場所"`
        Members   map[string][]string `json:"出席者"`
        Todo      []string            `json:"ToDo"`
        Decisions []string            `json:"決定事項"`
    }

    var m Meeting
    err := json.Unmarshal(data, &m)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(m)

}

うん、ちゃんとパース出来ているね。

Goでコマンドライン引数を処理する

おんなじファイルしか開けなかったり、常にラスト5行しか表示できないのは寂しいので、 コマンドライン引数を処理できるようにする。flagというパッケージを使えばよいらしい。

// コマンドライン処理
nFlag := flag.Int("n", 5, "Line numbers for display")
flag.Parse()
if flag.NArg() != 1 {
    fmt.Println("Err")
    return
}

content, err := ioutil.ReadFile(flag.Arg(0))
if err != nil {
    log.Fatal(err)
}

これでtail -n 5 filenameみたいな感じのコマンドラインには対応出来ているみたい。

ファイル名が指定されなかったときはエラーとして、簡単なメッセージだけで returnで終わっちゃってるんだけど、簡単に異常終了させる方法がたぶんあるはず。

まあ、ファイル名が指定されなかったらSTDINを処理するのが真っ当なんだけども、それは後回し。

Goでナイーブなtailを作る

私が日頃作るツールの大抵はフィルタなので、まずはcatコマンドが実装出来ることが必要。 とりあえず、ファイルをいったん全部読み込んで、最後の5行だけ書き出す、ナイーブな実装のtailコマンドを作ってみる。

パッケージのリストを眺めるとio/ioutil.ReadFileが良さそう。

サンプルをそのまま引き写す

package main

import (
    "fmt"
    "io/ioutil"
    "log"
)

func main() {
    content, err := ioutil.ReadFile("./sample.memo")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%s", content)
}

ReadFile()の戻りは(byte, error)である。 byteとstringの関係を知る必要がある。公式ドキュメントのstring型の説明を読むと、

string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. A string may be empty, but not nil. Values of string type are immutable.

てけとーな訳

string型は、8bitのbyteのあらゆる並びの集合です。伝統的には、UTF-8エンコードされたテキストを表現するものですが、 必ずしもそうである必要はありません。string型は空かもしれませんが、nilにはなりません。string型の値は、immutable(不変)です。

という説明なので、UTF-8のシーケンスであれば、byteと大差は無いのだろう。

とりあえず、byteはPrintf()を使えば文字列として表示できることはわかった。 これをPrintln()にすると、255以下の数字が並ぶことになる。 単純に型変換したらどうなるのだろう。fmt.Println(string(content))としたら、これでもちゃんと表示された。よしよし。

さて、これを各行に分解したい。すると、bufio.Scannerというのがあるみたい。 なるほど、いわゆるバッファドIOは、ioじゃなくて、bufioを使うのね。 ファイルを1行ずつ読み込むのはこれで良さそう。

ただし、今回は一挙に全部読み込みたいので、読み込んだものをsplitして各行にしたい。

regexpパッケージにSplitがあるので、たぶんこれでよいだろう。

main関数の中はこんな感じにした。regexpのimportは追加してある。

func main() {
    content, err := ioutil.ReadFile("./sample.memo")
    if err != nil {
        log.Fatal(err)
    }

    re := regexp.MustCompile(`\r?\n`)
    lines := re.Split(string(content), -1)
    noLines := len(lines)
    startidx := noLines - 5
    for i, v := range lines[startidx:] {
        fmt.Println(i+startidx+1, " : ", v)
    }
}

うん。ちゃんとラスト5行が取り出せている。おけ、おけ。

Goのチュートリアルをやる(4)

Goroutine

GoのGoたる所以だと、風の噂に聞いた。

  • 関数をgo付けて呼ぶと、Goroutineで動く
  • 通信するにはチャネル(型名はchan)を渡す
    • チャネルはmake(chan int, 255)のようにしてバッファに出来る。
    • チャネルはclose(ch)でクローズ出来る。しまっていることはv, ok := <-chで確認できる。
  • Mutexもある

Goのチュートリアルをやる(3)

メソッド

メソッドはレシーバーの型を指定した関数。その型を定義したパッケージでしか定義できない。

オブジェクト指向言語だとthisを使うようなところで、レシーバーの仮引数があるのが変わってるかもしれない。

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

thisを変更したい場合には、レシーバでポインタを受け取るようにする。

インターフェース

  • Rustでいうところのトレイト。
  • ただし、JavaやRustみたいに型が実装しているかを宣言はしない。ジェネリクスがないから。実装していない型に対して呼んじゃったら単に実行時エラー。
  • 値と型のタプルのように考えればいいらしい・・・どういう意味かよくわからない。
  • 空のインターフェースinterface{}を、Cのvoidのように使える

アサーションは、Cでいうキャスト。

var i interface{} = "hello"
t := i.(string)

とすると、tはstring型になる。

キャストに失敗するとパニックだが、第2の戻り値を取るとパニックにならない。

f := i.(float64) // => panic
f, ok := i.(float64) // 0, false
f, _ = i.(float64) // 0

Goのチュートリアルをやる(2)

More types

スライス

スライスが出てきた。スライスの概念はRustで初めて知ったけど、Goも大体同じ

  • 長さと容量を別に持ってる
  • スライスリテラル[]int{5, 4, 3, 2, 1}の様に作る。
  • スライスに要素を追加するには、append()を使う。容量を増やさないといけないときは、ギリギリじゃなくて余裕を持たせて(たぶん基本は倍)増やす。

構造体のスライスのリテラル

宣言と初期化をいっぺんにするのアリ。

s := []struct {
    i int
    b bool
}{
    {2, true},
    {3, false},
    {5, true},
    {7, true},
    {11, false},
    {13, true},
}

Map

キーに対して値をもつかどうかを2つめの戻り値でチェックできる

v, ok := m["key"]

いらなければ、

v := m["key"]

でよい。v, _で受けなくてよいのがGoのスタイルなのかな。

関数

  • 関数を変数に入れる場合の型の指定はfuncを使う。
  • 変数に入った関数のcallは普通に()をつける感じ。
  • Goの関数はクロージャ。環境をバインドする。
func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

func main() {
    hypot := func(x, y float64) float64 {
        return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(5, 12))

    fmt.Println(compute(hypot))
    fmt.Println(compute(math.Pow))
}

Goのチュートリアルをやる(1)

仕事でMarkdownっぽいフォーマットで議事録を書くと、ダサいフォーマットのHTMLに変換するスクリプトRubyで書いたら、gem installがsudoでしか出来ないと言われ、悲しんだ。

実行ファイルを作って配るのがやはり楽かもしれない。Rustでやろうかとも思ったけど、これを機にちょっとGoを触ってみることにする。 インストールはいつものようにbrew install goで。GOPATHやGOROOTは特に設定しろと言われなかったので、このままいってみる。

では、さっそくチュートリアル。日本語もあって嬉しい。

tour.golang.org

最初にページに書いてあるHello Worldを書いてみる。しかし、コンパイル方法が書いてない(笑)。

> pwd
/Users/tambara/study/go_study
> ls
hello.go
> cat hello.go 
package main

import "fmt"

func main(){
    fmt.Println("Hello, 世界")
}

goとだけ打つとUsageが出るので、それをみて、適当にgo buildしてみる

> go build
> ls -l
total 4128
-rwxr-xr-x  1 tambara  staff  2108040  3 31 09:52 go_study
-rw-r--r--@ 1 tambara  staff       76  3 31 08:54 hello.go
> ./go_study
Hello, 世界

実行ファイルできた。

> go clean
> ls 
hello.go

cleanで消えた。

そういえば、Rustではcargo runしたなと思い出し、runしてみる。

> go run
go run: no go files listed

なんか設定ファイルっぽいものが必要な様子。まあ、わからんでもない。気にせず進むことにする。

  • welcome
  • basics
  • flowcontrol

は特に問題なく進んだ。以下、ちょっと面白かったところ。

変数宣言

初期化しない変数宣言にはvarがいる。初期化がある場合には、

var i int = 1
var j = 2

としてもいいけど

i := 1

が簡潔で好まれるみたい。

制御構造

switch with no condition

switchで、条件を省略すると暗黙のtrueが置かれるというのは面白い。 というか、その場合は条件と値が逆の取扱になる。 かけ算の順序に厳しい小学校では烈火のごとく怒られそうだ(笑)。

以下のようにすると、最初にtrueになるcondの部分が実行される

switch {
case cond1:
    // cond1がtrueなら実行
case cond2:
    // cond1がfalse, cond2がtrueなら実行
default:
    // どちらもfalseなら実行
}

そういえば、何が真偽値になるのかの話はまだ読んでなかった。

Defer

関数の遅延評価。引数は渡した時点で評価されるが、関数の中身はreturnするまで評価されない。

var gs = "ぐろーばる"

func hoge(s string) {
    fmt.Println(gs + s)
}

func main(){
    s := "ろーかる"
    defer hoge(s)

    gs = "グローバル"
    s = "ローカル"
}

"グローバルろーかる"が返る。複数回deferするとスタックに積まれる。