20190728のGoに関する記事は9件です。

Golang 環境設定

いろんな場所に情報が...

Golang初心者が環境設定をする時に色んな記事をたらい回しにされたので、ここにまとめておく

開発環境

  • MacOS
  • Visual Studio Code
  • Github

gvmのインストール

gvmはGolangのバージョン管理ツール
下にある3つのコマンドを順番にシェルで実行すればgvmが使えるようになる

$ brew install mercurial
$ bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)
$ source $HOME/.gvm/scripts/gvm

3つ目のsource $HOME/.gvm/scripts/gvmは.bashrcに設定しておく

Golangのインストール

gvmを使ってインストールする

まずダウンロードできる Golang のバージョンのリストを下のコマンドで得る
gvm listall

今回はv1.12.7を下のコマンドで入れる
gvm install go1.12.7 -B

v1.12.7をデフォルトで使う設定を下のコマンドでする
gvm use go1.12.7 --default

なぜgvmを使う?

  • Golangを使った開発に必要なものを一箇所で管理できる
  • 複数のバージョンを gvm useコマンドで切り替えられる から

ワークスペースの設定

ワークスペースはGoを使った開発を行うディレクトリの事で、環境変数のGOPATHで指定する
今回は以下の場所をワークスペースとする

export GOPATH=$HOME/go

Golangのワークスペースには以下の3つのサブディレクトリが必要

  • bin: Goアプリのコンパイル後の実行ファイルを保管
  • pkg: 実行ファイルを作るための外部パッケージを保管
  • src: アプリ開発のソースコードを保管

ワークスペースの階層構造

3つのサブディレクトリを作った後の構成を下に示す

$HOME ─ go ┬ bin
           ├ pkg
           └ src ─ github.com ─ {github_id} - {repogitory_name} ─ .git
                                                                └ source code

上に示したように、src内にはリポジトリ情報を含んだ階層構造でプロジェクトを作成するのが一般的なようだ
{github_id}はあなたのGithubのアカウント名、{repogitory_name}はレポジトリの名前である
そして、{repogitory_name}内に.gitを作る

コーディングのサポートツール

公式でオススメされているサポートツールはgoplsというLanguage Serverのようだ
これを使うために、GolangのVSCodeの拡張機能であるGoを入れる

次にgoplsの設定を行う
まずgoplsを下のコマンドでインストールする
go get golang.org/x/tools/gopls

その後、settings.jsonに下の内容を追加後再起動すればLanguage Serverが起動する

settings.json
    "go.useLanguageServer": true,
    "go.alternateTools": {
        "go-langserver": "gopls"
    },
    "go.languageServerExperimentalFeatures": {
        "format": true,
        "autoComplete": true
    },
    "[go]": {
        "editor.snippetSuggestions": "none",
        "editor.formatOnSave": true,
        "editor.codeActionsOnSave": {
            "source.organizeImports": true
        },
    },
    "gopls": {
        "usePlaceholders": true,
        "enhancedHover": true
    },

パッケージ管理ツール

Golangのv1.11以降で採用されているgo modules(vgo)を用いる
これに関しては用意するものはなく、使い方を学ぶだけで良い

vgoを使うには以下の設定をsettings.jsonに追加する

settings.json
    "go.toolsEnvVars": {
        "GO111MODULE": "on"
    },

これでコード内でimportしている外部モジュールが、ビルド時にインストールされるようになる
これらのモジュールはビルド時に作られるファイルgo.modgo.sumで管理される

ビルド後動きに問題はないがimport文でエラーが出るので、これを表示させないために下の設定をsettings.jsonに追加する

settings.json
    "go.installDependenciesWhenBuilding": false,

ちゃんとできたか?

以上で設定はできているはず
後日気づいた点があれば追記していく予定

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golang ことはじめ

いろんな場所に情報が...

Golang初心者が開発環境を整える時に色んな記事をたらい回しにされたので、ここにまとめておく

開発環境

  • MacOS
  • Visual Studio Code
  • Github

達成目標

  • Golangのインストールとバージョン切り替えをスムーズに
  • コーディングサポート用のLanguage Serverを起動する
  • プロジェクト毎にパッケージを管理できる

gvmのインストール

gvmはGolangのバージョン管理ツール
下にある3つのコマンドを順番にシェルで実行すればgvmが使えるようになる

$ brew install mercurial
$ bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)
$ source $HOME/.gvm/scripts/gvm

3つ目のsource $HOME/.gvm/scripts/gvmは.bashrcに設定しておく

Golangのインストール

gvmを使ってインストールする

まずダウンロードできる Golang のバージョンのリストを下のコマンドで得る
gvm listall

今回はv1.12.7を下のコマンドで入れる
gvm install go1.12.7 -B

v1.12.7をデフォルトで使う設定を下のコマンドでする
gvm use go1.12.7 --default

なぜgvmを使う?

  • Golangを使った開発に必要なものを一箇所で管理できる
  • 複数のバージョンを gvm useコマンドで切り替えられる から

ワークスペースの設定

ワークスペースはGoを使った開発を行うディレクトリの事で、環境変数のGOPATHで指定する
今回は以下の場所をワークスペースとする

export GOPATH=$HOME/go

Golangのワークスペースには以下の3つのサブディレクトリが必要

  • bin: Goアプリのコンパイル後の実行ファイルを保管
  • pkg: 実行ファイルを作るための外部パッケージを保管
  • src: アプリ開発のソースコードを保管

ワークスペースの階層構造

3つのサブディレクトリを作った後の構成を下に示す

$HOME ─ go ┬ bin
           ├ pkg
           └ src ─ github.com ─ {github_id} - {repogitory_name} ┬ .git
                                                                └ main.go

上に示したように、src内にはリポジトリ情報を含んだ階層構造でプロジェクトを作成するのが一般的なようだ
{github_id}はあなたのGithubのアカウント名、{repogitory_name}はレポジトリの名前である
そして、{repogitory_name}内に.gitを作る

コーディングのサポートツール

公式でオススメされているサポートツールはgoplsというLanguage Serverのようだ
これを使うために、GolangのVSCodeの拡張機能であるGoを入れる

次にgoplsの設定を行う
まずgoplsを下のコマンドでインストールする
go get golang.org/x/tools/gopls@latest

その後、settings.jsonに下の内容を追加後再起動すればLanguage Serverが起動する

settings.json
    "go.useLanguageServer": true,
    "go.alternateTools": {
        "go-langserver": "gopls"
    },
    "go.languageServerExperimentalFeatures": {
        "format": true,
        "autoComplete": true
    },
    "[go]": {
        "editor.snippetSuggestions": "none",
        "editor.formatOnSave": true,
        "editor.codeActionsOnSave": {
            "source.organizeImports": true
        },
    },
    "gopls": {
        "usePlaceholders": true,
        "enhancedHover": true
    },

パッケージ管理ツール

Golangのv1.11以降で採用されているgo modules(vgo)を用いる
これに関しては用意するものはなく、使い方を学ぶだけで良い

vgoを使うには以下の設定を.bashrcに追加する

.bashrc
export GO111MODULE=on

これでコード内でimportしている外部モジュールが、ビルド時にインストールされるようになる
これらのモジュールはビルド時に作られるファイルgo.modgo.sumで管理される

go.modとgo.sumはプロジェクトごとに作るべきなので、プロジェクトディレクトリに入ってからアプリをビルドするのが良い
そうすると、go.modとgo.sumはプロジェクトディレクトリ内に作られる

ビルド後動きに問題はないがimport文でエラーが出るので、これを表示させないために下の設定をsettings.jsonに追加する

settings.json
    "go.installDependenciesWhenBuilding": false,

ちゃんとできたか?

以上で設定はできているはず
後日気づいた点があれば追記していく予定

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語のサンプルコード

学習履歴

■ はじめに

勉強したことはすぐ忘れてしまうので、サンプルコードをメモしておく。

新しく学んだことは追記していく予定。

■ 乱数の生成

乱数を生成方法を記述する。

rand を使えば乱数を生成できると思っていたが、うまくいかなかった。

generate_number.go
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    target := rand.Intn(100)
    fmt.Println(target)
}
$ go run generate_number.go 
81
$ go run generate_number.go 
81
$ go run generate_number.go 
81

ずっと 81 が表示される。

乱数を得たい場合は、rand.Seed に先に値を渡す必要がある。

この seed は、ランダムな番号を生成する generator だ。

ただし、この seed にも、プログラム実行毎に異なる値を渡して上げる必要がある。

generate_number.go
package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    // 現在の時刻を int 型で取得
    seconds := time.Now().Unix()
    rand.Seed(seconds)
    // 1 ~ 100 までの乱数を生成
    target := rand.Intn(100) + 1
    fmt.Println(target)
}
$ go run generate_number.go 
45
$ go run generate_number.go 
36
$ go run generate_number.go 
93

■ ファイルの読み込み

ファイルの読み込み、出力。(結構忘れる)

mkdir filereader
touch filereader/readfile.go
touch filereader/data.txt
data.txt
Hello,
This is,
Monkey.
readfile.go
package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
)

func main() {
    // Read a data file
    file, err := os.Open("data.txt")
    if err != nil {
        log.Fatal(err)
    }

    // Create New Scanner
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }

    err = file.Close()
    if err != nil {
        log.Fatal(err)
    }

    if scanner.Err() != nil {
        log.Fatal(scanner.Err())
    }
}
$ go run readfile.go
Hello,
This is,
Monkey.

■ 引数渡し

terminal からプログラムに引数を渡す。

main.go
package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Println(os.Args[1:])
}
$ go run main.go Arg1 Arg2 Arg3
[Arg1 Arg2 Arg3]

■ 可変長引数

main.go
package main

import "fmt"

func severalInts(numbers ...int) {
    fmt.Println(numbers)
}

func main() {
    severalInts(1)
    severalInts(1, 2)
    severalInts(1, 2, 3)
}
$ go run main.go 
[1]
[1 2]
[1 2 3]

■ 最大値の算出

main.go
package main

import (
    "fmt"
    "math"
)

func maximum(numbers ...float64) float64 {
    max := math.Inf(-1)
    for _, number := range numbers {
        if number > max {
            max = number
        }
    }
    return max
}

func main() {
    fmt.Println(maximum(1.2, 3.2, 3.5))
    fmt.Println(maximum(-23.1, -30.2, -3.2))
}
$ go run main.go 
3.5
-3.2

■ 境界値内の値の保持

min, max の境界値内の値を保持する。

main.go
package main

import (
    "fmt"
)

func inRange(min float64, max float64, numbers ...float64) []float64 {
    var result []float64
    for _, number := range numbers {
        if number >= min && number <= max {
            result = append(result, number)
        }
    }
    return result
}

func main() {
    fmt.Println(inRange(1, 100, -12.5, 10.3, 49, 101))
    fmt.Println(inRange(-10, 10, 5.2, 10.3, 4.9, 11))
}
$ go run main.go 
[10.3 49]
[5.2 4.9]

■ スライスを可変長引数に渡す

main.go
package main

import "fmt"

func severalInt(numbers ...int) {
    fmt.Println(numbers)
}

func mix(num int, flag bool, strings ...string) {
    fmt.Println(num, flag, strings)
}

func main() {
    intSlice := []int{1, 2, 3}
    severalInt(intSlice...)

    stringSlice := []string{"a", "b", "c", "d", "e"}
    mix(1, true, stringSlice...)
}
$ go run main.go 
[1 2 3]
1 true [a b c d e]

■ Map の初期値

Map の Key に存在しない Key の値を渡すと、bool 型が取得できる。

main.go
package main

import "fmt"

func main() {
    counters := map[string]int{"a": 3, "b": 0}
    var value int
    var ok bool
    value, ok = counters["a"]
    fmt.Println(value, ok)

    value, ok = counters["b"]
    fmt.Println(value, ok)

    value, ok = counters["c"]
    fmt.Println(value, ok)
}
$ go run main.go 
3 true
0 true
0 false

応用するとスライスの中身を数えることもできる。

main.go
package main

import (
    "fmt"
)

func main() {
    names := []string{
        "田中",
        "中山",
        "山田",
        "田中",
        "中山",
        "山田",
        "田中",
        "中山",
        "山田",
        "田中",
    }
    counts := make(map[string]int)
    for _, name := range names {
        counts[name]++
    }
    for name, count := range counts {
        fmt.Printf("Votes for %s: %d\n", name, count)
    }
}
$ go run main.go 
Votes for 中山: 3
Votes for 山田: 3
Votes for 田中: 4

■ まとめ

使えそうなコードをストックしておけば、実務でも役に立つときがある。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goで世界一シンプルなCUIライブラリを作りました

こんにちは。今日もいいターミナル日和ですね。
今回はGo製の新たなCUIライブラリ goban を作ったので、その紹介と簡単な使い方を説明します。

(追記)罫線と文字幅について

日本語環境では罫線が全角幅で表示されてしまい、綺麗に出力されない問題があります。その際は、以下の一行をMainの前に挿入してください。

    runewidth.DefaultCondition = &runewidth.Condition{EastAsianWidth: false}

ただし、これは ambiguous width に分類される文字すべてを半角幅で表示してしまうため、記号などを用いたときに別の問題が発生する場合があります。注意して使用してください。

今後、CJK環境では罫線を諦めてASCIIでボックスを表示する実装を入れる予定です。

CUIライブラリについて

ターミナルで動くグラフィカルなアプリを作るためのライブラリです。著名なものに gocui, tviewなどがあります。
gobanはこれらと比較してよりシンプルに、よりGoらしく書けることを意識しています。

gobanの特徴

  • シンプルながら強力なAPI
  • ボックス描画
  • グリッドレイアウト
  • チャネル経由でイベントを処理する。(not イベントハンドラ)
  • 分離されたビューとコントローラー
  • 容易なビューの組み合わせ

Hello, World!

READMEにも記載していますが、こちらがHello Worldです。

package main

import (
    "context"

    "github.com/eihigh/goban"
)

func main() {
    goban.Main(app, view)
}

func app(_ context.Context, es goban.Events) error {
    goban.Show()
    es.ReadKey()
    return nil
}

func view() {
    goban.Screen().Enclose("hello").Prints("Hello World!\nPress any key to exit.")
}

スクリーンショット 2019-07-29 18.07.38.png
簡単な解説:

  • goban.Main で画面を作成し、メイン処理を呼び出します。
  • app がユーザーが記述するメインの処理です。app が終了すると、Main も終了します。
  • goban.Show() で描画を行います。(後述)
  • es.ReadKey() はキーイベントを待って、読み取る関数です。キー以外のイベントは無視されます。
  • return nil でappを終了します。その後Mainも終了し、appの返したエラーを返します。

上述したとおり、イベントはチャネル経由で流れてきます。そのため、「ふつうの」プログラムのように流れ通りに処理を書いて、適時イベントを待ったり描画を行ったりする形になります。この辺はまさにGoのパワーが活きています。

描画システム

Hello Worldでは view という一つの描画関数のみを用いています。

  • goban.Main(app, view)view を描画関数として登録しています。
  • goban.Show() で描画関数が呼び出されます。

描画関数は複数登録でき、登録した順に呼び出されます。なので、ポップアップなどは以下のように適時ビューを追加することで、上にビューを重ねることができます。

func main() {
    goban.Main(app)
}

func app(_ context.Context, es goban.Events) error {
    // 下敷きとなるview
    v := func() {
        b := goban.Screen()
        b.Puts("Press any key to open popup")
        b.Puts("Ctrl+C to exit")
    }
    goban.PushViewFunc(v)

    for {
        goban.Show()
        es.ReadKey()
        popup(es)
    }
}

func popup(es goban.Events) {
    // ポップアップのview
    v := func() {
        b := goban.NewBox(0, 0, 40, 5).Enclose("popup window")
        b.Prints("Press any key to close popup")
    }

    // viewを上に追加する。
    // ポップアップのようにモーダルビューを用いるときは、
    // このように PushView と defer PopView をペアで使うのが推奨です。
    goban.PushViewFunc(v)
    defer goban.PopView()

    goban.Show()
    es.ReadKey()
}

ダウンロード.gif

ポイントは defer PopView で、これを用いることによって関数を脱出した際に自動的にビューを削除することができます。このように、通常状態遷移が必要な処理も、このように非常にシンプルに書くことができます。

グリッドレイアウト

gobanでは Box を用いてさまざまな描画を行います。その補助として、グリッドレイアウトを使用することができます。

var (
    grid = goban.NewGrid(
        "    1fr    1fr    1fr ",
        "1fr header header header",
        "3fr side   content ",
        "1fr footer footer footer",
    )
)

func main() {
    goban.Main(app, view)
}

func app(_ context.Context, es goban.Events) error {
    goban.Show()
    es.ReadKey()
    return nil
}

func view() {
    b := goban.Screen().Enclose("")
    header := b.GridItem(grid, "header").DrawSides("", 0, 0, 0, 1)
    header.Prints("Header")
    // 以下略
}

スクリーンショット 2019-07-29 18.05.48.png

NewGrid 関数にレイアウト文字列を渡して、直感的にグリッドレイアウトを作成できます。

  • 長さの単位には fr または em を使用できます。
  • 1行目には列のサイズを、2行目以降は行のサイズ+エリア名を記述します。同じエリア名を連続させることで、範囲を連結させることができます。
  • goban.Box#GridItem 関数で、グリッドに相当する範囲のボックスを取得します。

今回は文字列でグリッドを生成していますが、行と列の大きさを直接指定したり、範囲を直接指定して切り出したり(goban.Box#GridCell)することができます。

ビューモデル

ここまで、viewはすべて関数として解説してきましたが、実際には goban.View というインターフェースとして定義されています。(関数を渡すと内部でインターフェースを満たす型にキャストされています。)

このことを利用して、viewを状態を持つ型として定義することで、よりviewを強力にすることができます。

func main() {
    goban.Main(app)
}

func app(_ context.Context, es goban.Events) error {
    v := &menuView{
        items: []string{
            "foo", "bar", "baz",
        },
    }
    goban.PushView(v)

    for {
        goban.Show()
        switch k := es.ReadKey(); k.Key() {
        case tcell.KeyUp:
            v.cursor--
        case tcell.KeyDown:
            v.cursor++
        }
    }
}

// goban.Viewを実装している。
type menuView struct {
    cursor int
    items  []string
}

func (v *menuView) View() {
    b := goban.NewBox(0, 0, 50, 20).Enclose("menu")
    b.Puts("↑, ↓: move cursor")
    for i, item := range v.items {
        if i == v.cursor {
            b.Print("> ")
        }
        b.Puts(item)
    }
}

app側で menuView を作成し、viewとして追加しています。このパターンによって、

  • カーソル位置などの状態を持ちやすくなる。
  • appとviewをより独立に書くことができる。
  • appでview modelのスコープを明確にできる( defer PopView / defer RemoveView(v) と組み合わせてください)。

などのメリットが得られます。

以上、基本的な機能の解説でした!

これから

鋭意開発中ですが、基本機能のAPIはそう変わらない予定です。
今後はより見た目を改善する機能や(中央揃えなど)、エスケープシーケンスへの対応を広げたいです。

(追記) 試行錯誤中ですが、マウスサポートも追加しました。

また、拡張性を生かして、基本APIはそのままに別パッケージとして便利なウィジェットを多数提供する予定です。たとえば、

if confirm(events, msg) { /* 確認画面でOKされたときの処理 */ }
// goban.Eventsを渡せば描画から確認完了までやってくれるのがポイント!

など。(コードの見た目が Immediate GUI っぽくなりますね)

気になった方には是非examplesを覗いていただいて、気に入れば使っていただけると嬉しいです。p-rも歓迎です! goban、よろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebAssemblyから、Goのメソッドを呼び出す (2)

https://qiita.com/neko-suki/items/7fb822b9adfa6f1c12eb の続きです。

今回は、以下の2点を扱います。

  • Goで定義した構造体の定義は変更せずに、WebAssemblyからGoで定義した構造体のメソッドを呼ぶ
  • 構造体のメソッドの引数に、同じ構造体の別のインスタンスを渡す

両者とももっと賢く書く方法がある気がしますが、自分で調べた範囲では見つけられませんでした。

Goで定義した構造体の定義は変更せずに、WebAssemblyからGoで定義した構造体のメソッドを呼ぶ

特にいいやり方が思いつかなかったので、ラッパーを書きました。

構造体の定義

構造体の定義は元のままです。これをWebAssemblyから使えるようにします。元の構造体事態は何らかの事情で変更したくない場合を想定しています。

type Test struct {
    Num int
}

func (t *Test) Print() {
    fmt.Println(t)
}

func (t *Test) Twice() {
    t.Num *= 2
}

func (t *Test) Add(val int) {
    t.Num += val
}

func (t *Test) GetNum() int {
    return t.Num
}

WebAssembly用の実装

WebAssembly側から呼び出せるようにラッパー関数をそれぞれ書きました。

package main

import (
    "syscall/js"
)

func (t *Test) PrintWrapper(this js.Value, args []js.Value) interface{} {
    t.Print()
    return nil
}

func (t *Test) TwiceWrapper(this js.Value, args []js.Value) interface{} {
    t.Twice()
    return nil
}

func (t *Test) AddWrapper(this js.Value, args []js.Value) interface{} {
    t.Add(args[0].Int())
    return nil
}

func (t *Test) GetNumWrapper(this js.Value, args []js.Value) interface{} {
    return js.ValueOf(t.GetNum())
}

func registerCallbacks() {
    var test = &Test{
        Num: 1,
    }
    js.Global().Set("test", js.ValueOf(
        map[string]interface{}{
            "print":  js.FuncOf(test.PrintWrapper),
            "twice":  js.FuncOf(test.TwiceWrapper),
            "add":    js.FuncOf(test.AddWrapper),
            "getNum": js.FuncOf(test.GetNumWrapper),
        },
    ))
}

func main() {
    c := make(chan struct{}, 0)
    registerCallbacks()
    <-c
}

構造体のメソッドの引数に、同じ構造体の別のインスタンスを渡す

例えば、以下のように同じ構造体の別のインスタンスを受け取って内部で利用する場合を考えます。

func (t *Test) AddAnotherTest(t2 *Test) {
    t.Num += t2.Num
}

WebAssemblyの実装

まずは、関数登録部分です。構造体自信のポインタを取得するための関数 _ptrを追加しています。これによって、登録した構造体のインスタンスがjavascript側から引数として渡されたときにポインタを取得できるようになります。_ptrに直接ポインタの値を代入しないのはjavascript側で値を改変されないようにするためです。(プライベートメンバとして登録できれば必要ないはずですが、今回はそこまで調べていないです。)

func registerCallbacks() {
    var test = &Test{
        Num: 1,
    }
    js.Global().Set("test", js.ValueOf(
        map[string]interface{}{
            "_ptr":           js.FuncOf(test.getPtr),
            "print":          js.FuncOf(test.PrintWrapper),
            "twice":          js.FuncOf(test.TwiceWrapper),
            "add":            js.FuncOf(test.AddWrapper),
            "getNum":         js.FuncOf(test.GetNumWrapper),
            "addAnotherTest": js.FuncOf(test.AddAnotherTestWrapper),
        },
    ))

    var test2 = &Test{
        Num: 2,
    }
    js.Global().Set("test2", js.ValueOf(
        map[string]interface{}{
            "_ptr":           js.FuncOf(test2.getPtr),
            "print":          js.FuncOf(test2.PrintWrapper),
            "twice":          js.FuncOf(test2.TwiceWrapper),
            "add":            js.FuncOf(test2.AddWrapper),
            "getNum":         js.FuncOf(test2.GetNumWrapper),
            "addAnotherTest": js.FuncOf(test2.AddAnotherTestWrapper),
        },
    ),
    )
}

次にポインタ取得用関数の実装です。unsafe.Pointerjs.ValueOf()js.Value型に変換して、javascript側に渡します。

func (t *Test) getPtr(this js.Value, args []js.Value) interface{} {
    return js.ValueOf(unsafe.Pointer(t))
}

最後に、構造体を受け取って処理する部分です。javascript側からtest.AddAnotherTest(test2)という形で呼ばれる想定です。

まず、args[0].Get('_ptr')test2._ptr関数を呼び出しして、test2のポインタを取得します。ただしこの段階ではjs.Value型なので変換が必要です。unsafe.Pointerはfloat64で実装されているようなので、.Float()を呼び出します。その結果をunsafe.Pointer型に変換して、最後にTestのポインタに変換します。

func (t *Test) AddAnotherTestWrapper(this js.Value, args []js.Value) interface{} {
    ptrJSValue := args[0].Call("_ptr")
    t2 := (*Test)(unsafe.Pointer(ptrJSValue.Float()))
    t.AddAnotherTest(t2)
    return nil
}

テスト用のhtml

テスト用に使うhtmlです。addAnotherTest() というのが追加されています。例えば、twice, addAnotherTest, printという順番でボタンを押すと、consoleに&{4}が表示されます。

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Go wasm</title>
</head>

<body>
    <script src="wasm_exec.js"></script>
    <script>
        const go = new Go();
        WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
            go.run(result.instance);
        });
    </script>
    <script>
        function getNum(){
            console.log(test.getNum())
        }
        function addAnotherTest(){
            console.log(test2)
            test.AddAnotherTest(test2)
        }
    </script>
    <button onClick="test.print();" id="runButton" >print</button><br>
    <button onClick="test.twice()" id="runButton">twice</button><br>
    <button onClick="test.add(1)" id="runButton">add</button><br>
    <button onClick="getNum()" id="runButton">get test.Num</button><br>
    <button onClick="addAnotherTest()" id="runButton"> addAnotherTest</button><br>

</body>
</html>

ソース

下に置いてあります。
https://github.com/neko-suki/wasm_struct

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gRPC UIを使ってみんなが触れるgRPCの遊び場を作りました

gRPCは近年非常によく使われるようになったRPCフレームワーク1ですが、その柔軟なメッセージフォーマットに対応するリッチなGUIクライアントに欠けていました。REST APIでいうPostman的な存在です。このようなツールがなければgRPCを利用した開発が面倒なのですが、その問題は現在においてはほぼ払拭されたといっても過言ではありません。gRPC UIが登場したからです。

(本記事は自分のブログからの転載記事です。)

TL;DR

  • gRPC UIで作成した遊び場はこちらです :point_right: gRPC UI playground
  • gRPC UIはgRPCサーバへの要求と応答がWebで簡単にできるので、今後のgRPCを利用した開発に広く使われるツールとなりそうです
  • gRPC UI playgroundは以下のサービスを利用して、無料で立ち上げました。当面はみんなが遊べるようにしておくつもりです

はじめに

本記事はgRPCのWeb UIであるgRPC UIの紹介記事です。gRPCに興味がある方、gRPCを利用している方を対象にしています。実際にgRPC UIを触れる遊び場も作ったのでgRPCに興味がある方は遊んでいってください。また本記事ではこの遊び場をどうやって作ったのかも簡単に解説します。

gRPCとは

gRPCはGoogleが開発してオープンソースとして公開したRPCフレームワークです。IDL2としてProtocol Buffers3が利用可能です4。gRPCは基本的には以下の図ように単純で、クライアントから要求(Proto Request)を送ってサーバが応答(Proto Response)を返すというものです。クライアントやサーバの記述には、Protocol Buffersが対応している様々な言語(C++, Ruby, Java等)が利用可能です。

What is gRPCより引用

gRPCは上記のように単純な要求/応答型以外にも様々なやりとりに対応できます。具体的には以下の4つです。

  • Unary RPC(単純な要求/応答型)
  • Server streaming RPC(サーバが複数の応答を返すことができる)
  • Client streaming RPC(クライアントが複数の応答を返すことができる)
  • Bidirectional streaming RPC(クライアントとサーバがそれぞれ複数の要求/応答ができる。一般的な双方向通信)

上記のような柔軟な呼び出しはHTTP/25の基盤に支えられて実現されています。

gRPC UIについて

gRPC UIはgRPCサーバとブラウザでやりとりできるツールです。百聞は一見にしかず。以下の画面を御覧ください。

この画面を見て震えたのはやはり要求の入力画面(Request Form)の充実っぷりです。gRPCは自分でデータの型を定義して入れ子のようなメッセージを定義できますが、そのような入力も簡単にできます。上記の図のではtest.TestMessageというメッセージの中にperson(test.Personというメッセージの型)が内包されています。また複数のデータが入力可能なフィールドは「Add Item」で簡単に追加できます。その他の注目すべき点は赤字で説明を入れておいたので参考にしてください。基本的にはgRPC(Protocol Buffers)で定義されている入力形式はほぼ網羅されています。

また、驚いたのはgRPCには基本的なスカラ値(stringint64等)以外にも「Well-Known Types」と呼ばれる型が定義されていますが、gRPC UIにはこれらにもリッチなインタフェースが提供されていたことです。以下の画面はtimestamp型に対するインタフェースです。

またリッチなフォームだけではなく、JSON形式でリクエストを投げることもできます。

JSON形式で便利なのはコピペが容易なところです。また省略されているフィールドは出力されないので見やすくなります。フォーム型の編集画面とも連動しているので、非常に使いやすいです。リクエストは「Invoke」ボタンを押すことでサーバに投げることができます。このボタンはフォーム型のリクエストにも一番下にあります。以下が「Invoke」を押して返ってきた応答になります。

この画面も特に説明がいらないくらい分かりやすいものになっています。gRPC UIで対応できないのは双方向の複雑なやり取りです。これは従来どおりCUI等を利用して行うしかありません。

gRPC UIで遊ぼう! 〜 gRPC UI playground 〜

さて、ここまで来たらgRPC UIで遊んでみたくなったと思います。以下にみんなが遊べるように「遊び場」を立ち上げたのでぜひいろいろ遊んでみてください。

gRPC UIはバックグラウンドで起動しているgRPC UIのテストサーバ(testsvr)とやりとりしています。プロトコルは「test.proto」、サーバの実装は「testsvr.go」に記述されています。実際にgRPC UIを動かしながらソースやIDLを読むとgRPCの理解が深まると思います。

メソッドに関しては「DoManyThings」が全部詰めになっていて、残りのメソッドはそれを機能単位で分割したものになっています。たぶん・・・

grpcui-playgroundの作り方

さて、ここからはおまけです。上記の遊び場(以降、grpcui-playgroundと記載)の構築方法の興味がある方を対象に簡単に説明したいと思います。grpcui-playgroundは3つのサービスを利用して無料で立ち上げました。以下がその構成になります。

上の図をざっくり説明すると、基本的にはDockerを使ってサービス提供しています。一つのコンテナの中にgPPC UIとgRPC Server(testsvr)という二つのプロセスが動いています。一つのコンテナに二つのプロセスはあまりお行儀がよくありませんが、今回は遊び場ということと無料で立ち上げることを重視したのでこのような構成になっています。
利用したコンテナホスティングサービスはArukas6になります。今回はFreeプランを利用しています。Arukasにコンテナイメージを提供するためにはDocker Hubも利用しました。Docker HubにはGitHubと連携しておくとGitHubの変更を検知してコンテナをビルドする機能があるのでそれも利用しています。

Dockerfileを書く

Dockerfileを書くときの注意点は二つです。まず、ビルドイメージを小さくするためにAlpine Linux7をベースイメージにします。次にgRPC UIが提供しているテストサーバ(testsvr)はgoで書かれているのでgoのビルド環境が必要です。ただしgoのビルド環境は動作には必要ないのでマルチステージビルドを活用して、なるべく小さなイメージにするようにしましょう8

Dockerfile
FROM golang:1.12.7-alpine as build-env
MAINTAINER hinastory

WORKDIR /testsvr
COPY testsvr /testsvr

RUN apk update && apk add --no-cache git

RUN go build

RUN go get -x github.com/fullstorydev/grpcui
RUN go install -x github.com/fullstorydev/grpcui/cmd/grpcui

FROM alpine
WORKDIR /
COPY --from=build-env /testsvr/testsvr /bin/testsvr
COPY --from=build-env /go/bin/grpcui /bin/grpcui
COPY start.sh /start.sh

EXPOSE 8080

ENTRYPOINT [ "/start.sh" ]

GitHubでDockerfileを公開する

ここは特に説明はしませんが、GitHubに公開レジストリを作成し、Dockerfileとtestsvrのソースと起動スクリプトをpushします。pushした自分のリポジトリは以下になります。

Docker Hubでイメージをビルド & 公開する

Docker Hubでイメージをビルドするのには、GitHubと連携しておいたほうが楽です。GitHubと連携しておけばGitHubのレポジトリをDocker HubのUIから選択できるようになり、GitHubにpushするだけでDockerfileをビルドしてくれるようになります。

注意すべき点はDockerイメージのタグの付け方です。デフォルトだとブランチを監視してlatestタグを付けるようになっていますが、Arukasはイメージをキャッシュする場合があるようなのでバージョンが入った適切なタグをつけることが望ましいです。そしてDockerのイメージタグはGitHubのタグと連動しているとよいと思われます。

Docker Hubでその設定をするのは簡単でDockerHubのリポジトリのBuildsの画面からビルドルールを設定でき、Source TypeTagにして、SourceDocker Tagを正規表現で記述することでお望みのタグが構成できると思います。またここでDockerfileの位置やビルドコンテキストも指定可能なので、少し複雑なリポジトリ構成でも対応出来ます。

ここの設定が終わったらGitHubにタグをpushしてビルドが始まるか確認します。基本的には検知はすぐに行われてIn Progressの状態にすぐに変わりますがビルドが終わるまでには時間がかかるので、コーヒーでも飲んでまったりと待ちましょう(笑)。自分のコンテナイメージは以下に公開してあります。

ArukasでDockerコンテナを公開する

最後にArukasuでコンテナを公開します。FreeプランはO.1vCPUと128MB RAMの非力な環境ですが、今回のような遊び場には充分だと思われます。また「転送量課金」がないのでDDoSとかの心配がまったくいらないのも嬉しいところです。もちろん最悪サービスは落とされますが、料金に怯える心配は無いわけです。ちなみにFreeプランでも電話認証とクレジットカードの登録は必須です。このアカウント登録が最大の難関でコンテナの起動は驚くほどあっけなく終わりました。アプリ起動の詳細は「アプリの作成 – Arukas Help Center」を参照してください。以下は起動したコンテナの管理画面です。Endpointはアプリの再起動で変わることが無いURLです。Portはインスタンスの起動毎に変わります。ちなみにgRPC UIは8080ポートでHTTPでサービスを公開していますが、エンドポイントではHTTPSになっているので自動でArukasがHTTPSに包んでくれるみたいです。

まとめ

本記事では、gRPCサーバとブラウザでやり取りできるgRPC UIを紹介しました。gRPC UIは非常に便利なので今後gRPC関連の開発で広く使われていくものと思われます。そして実際にgRPC UIを触れる遊び場を作成して以下に公開しました。

また、おまけとしてその遊び場の作り方を(無料でコンテナサービスを立ち上げる手順)を簡単に説明しました。
本記事がgRPCを理解し、より便利に使えるようになるための一助になれば幸いです。

参考文献


  1. RPC(Remote Procedure Call)は、簡単に言えばプログラムの中から内部の関数を呼ぶのと似たような感覚で、外部のネットワーク上の関数や手続きを呼べるようにする技術のことです。他のRPCにはSOAPやJSON-RPCなどがあります。 

  2. IDLはインタフェース記述言語(Interface Description Language)のことです。簡単に言えば関数呼び出しの宣言部分を定義する際に使う言語です。 

  3. Protocol BuffersはGoogleが開発したシリアライズフォーマットです。IDLは独自の言語でシンプルで分かりやすいのが特徴です。IDLのファイルは.protoの拡張子を持っており、protocというコンパイラを用いてIDLファイルからGo言語やRuby、Python、Java等様々な言語のバインディングを生成できます。現在のProtocol Buffersのバージョンは3になります。 

  4. JSONも利用可能みたいですが、自分は使ったことはありません。 

  5. HTTP2はHTTP/1.1の後継バージョンでRFC7540で定義されています。HTTP/1.1と比べてヘッダーの圧縮やバイナリメッセージ構造、双方向通信のサポート等様々な改善がなされています。 

  6. Arukasはさくらインターネットが提供しているDockerのホスティングサービスです。Arukasを反対から読むと・・・ 

  7. Alpine Linuxは軽量、シンプル、セキュアをコンセプトにしたLinuxディストリビューションです。組み込み系で実績のあるmusl libcとbusyboxをベースにしています。その軽量さからコンテナのベースイメージとしてよく利用されています。 

  8. 最初は無邪気にベースイメージをstretchにしてシングルステージでビルドしたため、イメージのサイズが400MBを超えてしまいました・・・現在のサイズは 18MBです。特にパブリックなレジストリに登録する際はコンテナイメージのサイズに充分気を配り、リソースを無駄にしないように心掛けましょう(自戒)。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebAssemblyから、Goのメソッドを呼び出す

試したこと

WebAssemblyで、Goで宣言した構造体のメソッドを呼び出します。環境は、go1.12.7 + Chromeで確認しています。

ちなみに、syscall/jsを使っているので、GOOS=js GOARCH=wasm 以外で使うことはたぶん難しいです。

構造体の定義

確認用に、Numというメンバを持ったTestという構造体を使います。メソッドは、Testを出力するPrint()と、Numの値を2倍にするTwice()Test.Numに値を足すAdd()、値を取得するGetNum()を用意します。

type Test struct {
    Num int
}

func (t *Test) Print() {
    fmt.Println(t)
}

func (t *Test) Twice()  {
    t.Num *= 2
}

func (t *Test) Add(val int) {
    t.Num += val
}

func (t *Test) GetNum() int {
    return t.Num
}

コマンドラインで実行してTest構造体を使ってみます。

package main

import (
    "fmt"
)

type Test struct {
    Num int
}

func (t *Test) Print() {
    fmt.Println(t)
}

func (t *Test) Twice()  {
    t.Num *= 2
}

func (t *Test) Add(val int) {
    t.Num += val
}


func (t *Test) GetNum() int {
    return t.Num
}

func main() {
    var test = &Test{
        Num: 1,
    }
    test.Print() // result should be &{1}
    test.Twice() 
    test.Print() // result should be &{2}
    test.Add(1)  
    test.Print() // result should be &{3}
    fmt.Println(test.GetNum()) // result should be 3
}

実行結果は↓のようになります。

&{1}
&{2}
&{3}
3

WebAssembly用にコードを修正

コードの全体です。

package main

import (
    "fmt"
    "syscall/js"
)

type Test struct {
    Num int
}

func (t *Test) Print(this js.Value, args []js.Value) interface{} {
    fmt.Println(t)
    return nil
}

func (t *Test) Twice(this js.Value, args []js.Value) interface{} {
    t.Num *= 2
    return nil
}

func registerCallbacks() {
    var test = &Test{
        Num: 1,
    }
    js.Global().Set("test", js.ValueOf(
        map[string]interface{}{
            "Print": js.FuncOf(test.Print),
            "Twice": js.FuncOf(test.Twice),
        },
    ))
}

func main() {
    c := make(chan struct{}, 0)
    registerCallbacks()
    <-c
}

コードの解説

WebAssemblyから呼び出せるように関数の定義を変更する

WebAssemblyから呼び出すときの作法に従い、引数をjs.Valueargs[]js.Value、 返り値をinterface{}にします。

func (t *Test) Print(this js.Value, args []js.Value) interface{} {
    fmt.Println(t)
    return nil
}

引数を参照する

引数の値を直接Go側で使うことはできないので、args[0].Int()を呼び出して整数型に変換します。

func (t *Test) Add(this js.Value, args []js.Value) interface{} {
    t.Num += args[0].Int()
    return nil
}

返り値を戻す

返り値はそのまま返せないので、js.ValueOf()で返します。

func (t *Test) GetNum(this js.Value, args []js.Value) interface{} {
    return js.ValueOf(t.Num)
}

構造体のメソッドのjavascriptへの登録

js.Global()を呼び出して、javascriptの空間にtest.Printtest.Twicetest.Addtest.GetNumをそれぞれ登録します。

func registerCallbacks() {
    var test = &Test{
        Num: 1,
    }
    js.Global().Set("test", js.ValueOf(
        map[string]interface{}{
            "print":  js.FuncOf(test.Print),
            "twice":  js.FuncOf(test.Twice),
            "add":    js.FuncOf(test.Add),
            "getNum": js.FuncOf(test.GetNum),
        },
    ))
}

関数の登録

関数の登録を呼び出します。main関数が終了しないように、チャネルを使って処理をブロックします。

func main() {
    c := make(chan struct{}, 0)
    registerCallbacks()
    <-c
}

テストする

以下の、htmlファイルでテストをします。

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Go wasm</title>
</head>

<body>
    <script src="wasm_exec.js"></script>
    <script>
        const go = new Go();
        WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
            go.run(result.instance);
        });
    </script>
    <script>
        function getNum(){
            console.log(test.getNum())
        }
    </script>
    <button onClick="test.print();" id="runButton" >print</button><br>
    <button onClick="test.twice()" id="runButton">twice</button><br>
    <button onClick="test.add(1)" id="runButton">add</button><br>
    <button onClick="getNum()" id="runButton">get test.Num</button><br>
</body>
</html>

WebAssemblyの実行に使う wasm_exec.js (go1.12.7用)は、公式から落とせます。

また、Goのファイルは、GOOS=js GOARCH=wasm go build -o test.wasm でビルドしておきます。

上記のhtmlファイル、wasm_exec.js, test.wasm をサーバ上に置いてアクセスすると、以下のようにボタンが表示されます。html, wasm_exec.js, test.wasm

キャプチャ.PNG

コマンドラインで確認したときと同様に、printtwiceprintaddprintget test.Num を押すと、ブラウザのコンソールに以下のように表示されました。

&{1}
&{2}
&{3}
3

構造体は直接渡せない?

構造体を直接登録するのは、試してみましたができないようでした。panic: ValueOf: invalid valueが出ます。js.ValueOf()の実装を見ると、任意の構造体は扱えない感じになっていました。

func registerCallbacks() {
    var test = &Test{
        Num: 1,
    }
    js.Global().Set("test", js.ValueOf(test))
}

ソース

githubに置いておきました。

https://github.com/neko-suki/wasm_struct

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GolangとDBを接続して株価データをチャート表示する

酒井潤さんのUdemy講座を受講していて、
自分でもGolangとDBを接続して、株価データをチャート表示したくなったので、方法をまとめてみました。

講座ではsqlite3を使用されていましたが、僕はSQLServerを使用しています。基本的にGolangでは一般的なDB(msSqlとか)の接続方法は大体同じだと思います。

※ソース内のコードはエラーハンドリングとかimportとかは省略していますので、悪しからず。

実装例(4565:そーせいのチャート)

image.png

テーブル構造

image.png

main.go

main.go
func main() {
    controllers.StartWebServer()
}

webserver.go(contorllers>webserver.go)

webserver.go
var templates = template.Must(template.ParseFiles("app/views/data.html"))

func dataHandler(w http.ResponseWriter, r *http.Request) {
    //4565:そーせいのデータを表示
    df, _ := models.GetData("4565")
    err := templates.ExecuteTemplate(w, "data.html", df)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

func StartWebServer() error {
    http.HandleFunc("/data/", dataHandler)
    return http.ListenAndServe(":8080", nil)
}

template.Must(template.ParseFiles("app/views/data.html"))

template.Mustはtemplate.ParseFiles(~)で返されたtemplateのポインタ値をエラーをラッピングして、
templateだけを返してくれます。

df, _ := models.GetData("4565")

GetDataに株価コードを渡すようにしました。

stockdata.go(models>stockdata.go)

stockdata.go
type StockData struct {
    StockCode string  `json:"stockcode"`
    Prices    []Price `json:"price"`
}

type Price struct {
    Time   time.Time `json:"time"`
    Open   float64   `json:"open"`
    Close  float64   `json:"close"`
    High   float64   `json:"high"`
    Low    float64   `json:"low"`
    Volume int32     `json:"volume"`
}

func GetData(stockcode string) (df *StockData, err error) {
    db, err := sql.Open("sqlserver", "server=.;user id=sa;password=password;database=Stock")
    defer db.Close()

    cmd := fmt.Sprintf(`SELECT ~`)
    rows, err := db.Query(cmd)

    df = &StockData{}
    for rows.Next() {
        var price Price
        rows.Scan(&price.Time, &price.Open, &price.Close, &price.High, &price.Low, &price.Volume)
        df.Prices = append(df.Prices, price)
    }
}

db, err := sql.Open("sqlserver", "server=.;user id=sa;password=password;database=Stock")

sql.Openの第一引数に"sqlserver"、第二引数に接続文字列をセットします。

data.html

data.html
 <!DOCTYPE html>
 <html>
 <head>
     <title>Page Title</title>
     <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
     <script type="text/javascript">
        google.charts.load('current', {'packages':['corechart']});
        google.charts.setOnLoadCallback(drawChart);

        function drawChart() {
            var data = google.visualization.arrayToDataTable([
                {{ range .Prices }}
                ['{{.Time}}'.substring(0, 10),{{ .Low }},{{ .Open }},{{ .Close }},{{ .High }}],
                {{end}}
            ], true);

            //お好みで設定
            var options = {...}

            var chart = new google.visualization.CandlestickChart(document.getElementById('chart_div'));

            chart.draw(data, options);
        }
    </script>

 </head>
 <body>
 {{ .StockCode }}
 <div id="chart_div" style="width: 900px; height: 500px;"></div>
 </body>
 </html>

[options]に設定する内容はこちら

{{ range .Prices }}

Prices([]Price)をループさせます。

['{{.Time}}'.substring(0, 10),{{ .Low }},{{ .Open }},{{ .Close }},{{ .High }}],

ループ中の現在のPrice構造体から日付、安値、始値、終値、高値を順にセットします。
※{{.Time}}から日付(yyyy/MM/dd)だけを抽出する良い方法が浮かばなかったので、substringで対応しました。(良い方法をご存知の方は教えていただけると幸いです)

{{end}}

ループを終わります。

これで株価データのチャート表示ができるようになりました。今後はフロントエンド、バックエンド部分の機能拡張を進めていきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goで設計に役立つEnumの実装を考える

はじめに

JavaでEnumを使った設計パターンはよくあるんですが、Goで同等のことをどうやるか考えてみました。

料金区分を例に

現場で役立つシステム設計の原則ではJavaのEnumを使った料金区分の設計と実装が紹介されています。
この例を参考にGoで適用するとどうなるか考えてみました。

まずは本と同様に料金区分をinterfaceで実装します。

type Fee interface {
    Yen() int
    Label() string
}

type adultFee struct{}

func (adultFee) Yen() int {
    return 1500
}

func (adultFee) Label() string {
    return "大人"
}

type childFee struct{}

func (childFee) Yen() int {
    return 1000
}

func (childFee) Label() string {
    return "子供"
}

料金区分(Fee)をinterface
大人料金(adultFee)と子供料金(childFee)はFeeのinterfaceを満たしています。

設計でJavaのEnumを使う魅力

  • 定義したものだけを呼べて、定義していないものを呼び出すとコンパイルエラーになる。
  • valueOfメソッドで文字列からenumの値を取得できる

GoでのEnum実装 - 変数で定義

Goで実装してみます。

var FeeType = struct {
    Adult   Fee
    Child   Fee
    ValueOf func(s string) (fee Fee, err error)
}{
    Adult: adultFee{},
    Child: childFee{},
    ValueOf: func(s string) (fee Fee, err error) {
        switch s {
        case "adult":
            fee = adultFee{}
        case "child":
            fee = childFee{}
        default:
            err = fmt.Errorf("未定義の料金タイプ %s", s)
        }
        return
    },
}

ポイントは定義したものだけを呼び出せるようにstructのメンバーとして定義済みの値を用意しました。
また、valueOfについてもstructのメンバーとして追加しています。

GoでのEnum実装 - 関数で定義

変数での定義が気になる方は、こんな方法も

func FeeType() struct {
    Adult   Fee
    Child   Fee
    ValueOf func(s string) (fee Fee, err error)
} {
    return struct {
        Adult   Fee
        Child   Fee
        ValueOf func(s string) (fee Fee, err error)
    }{
        Adult: adultFee{},
        Child: childFee{},
        ValueOf: func(s string) (fee Fee, err error) {
            switch s {
            case "adult":
                fee = adultFee{}
            case "child":
                fee = childFee{}
            default:
                err = fmt.Errorf("未定義の料金タイプ %s", s)
            }
            return
        },
    }
}

変数と違い関数なので間違っても上書きされないので安心です。

まとめ

今回はGoでJavaのEnum相当の機能を作り込む方法を考えてみました。
こんなやり方もあるよという方がいればコメント欄などで教えてもらえると嬉しいです。

参考

GoでEnumを定義する 〜intやstringよりも厳密に〜

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む