Quantcast
Browsing Latest Articles All 25 Live
Mark channel Not-Safe-For-Work? (0 votes)
Are you the publisher? or about this channel.
No ratings yet.
Articles:

redisを扱うコードをユニットテストする #golang

はじめに

この記事は、Go3 AdventCalendarの1日目の記事です。

RedisやMySQLを扱うような技術的実装については、interfaceで抽象化するといった使用コード側のユニットテストノウハウはよく見られますが、技術的実装自体をテストする方法はノウハウがあまり見られないなと思いますので、今回はそういう場合のテストの方法について紹介できればと思います。

なお、今回紹介するコードは、https://github.com/hgsgtk/go-snippets/pull/16 にて公開しています。

.
├── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
├── persistence
│   └── kvs
│       ├── client.go
│       └── client_test.go
└── testhelper
    └── mock_redis_client.go

今回は、redis-client実装にあたり、次のライブラリを利用しています。

https://github.com/go-redis/redis

redisを扱う実装をする

詳細な説明はコード内に記載いたしますが、トークンで値を取得するような実装を例として考えていきます。

persistence/kvs/client.go
package kvs

import (
    "strconv"
    "time"

    "github.com/go-redis/redis"
    "github.com/pkg/errors"
)

// 値取得の際になかった場合はredis.Nilがerror型として返ってくる
// エラーハンドリングのためinternalなpackageに定義する
const Nil = redis.Nil

// New create instance of redis.Client
func New(dsn string) (*redis.Client, error) {
    client := redis.NewClient(&redis.Options{
        Addr:     dsn,
        Password: "",
        DB:       0,
    })
    if err := client.Ping().Err(); err != nil {
        return nil, errors.Wrapf(err, "failed to ping redis server")
    }
    return client, nil
}

// 今回はトークンベースで値を取得するような例で実装してきます。
const (
    tokenKey      = "TOKEN_"
    tokenDuration = time.Second * 3600
)

// SetToken set token and value
func SetToken(cli *redis.Client, token string, userID int) error {
    if err := cli.Set(tokenKey+token, userID, tokenDuration).Err(); err != nil {
        return err
    }
    return nil
}

// SetToken set token and value
func GetIDByToken(cli *redis.Client, token string) (int, error) {
    v, err := cli.Get(tokenKey+token).Result()
    if err != nil {
        return 0, errors.Wrapf(err, "failed to get id from redis by token")
    }
    id, err := strconv.Atoi(v)
    if err != nil {
        return 0, errors.Wrapf(err, "failed to convert string to int")
    }
    return id, nil
}

ユニットテストする

上記のコードをユニットテストするには大きく2つ手段があるかと思います。

  1. *redis.Clientをwrapしてinterfaceを作成しモック差し替え可能とする
  2. ユニットテスト用のredis serverを用意する

選択肢1を選択する場合の例として、go-redis/redis のissueで紹介されています。
https://github.com/go-redis/redis/issues/332

今回は、選択肢2をやっていきます。それにあたって、次のライブラリを使用します。
https://github.com/alicebob/miniredis

miniredisは、ユニットテスト用のredisモックサーバを作れるライブラリです。これを使ってテストを書いていきます。

テスト用redisサーバ

テスト用redisサーバを用意するテストヘルパーを作っていきます。念の為の説明ですが、テストヘルパーを作るには、*testing.Tを渡した上で、t.Helper()と書きます。テストヘルパーとして作ることで、これを使用したテストのエラー箇所がわかりやすくなります。

testhelper/mock_redis_client.go
package testhelper

import (
    "testing"

    "github.com/alicebob/miniredis"
    "github.com/go-redis/redis"
)

func NewMockRedis(t *testing.T) *redis.Client {
    t.Helper()

    // redisサーバを作る
    s, err := miniredis.Run()
    if err != nil {
        t.Fatalf("unexpected error while createing test redis server '%#v'", err)
    }
    // *redis.Clientを用意
    client := redis.NewClient(&redis.Options{
        Addr:     s.Addr(),
        Password: "",
        DB:       0,
    })
    return client
}

テストをする

上で作成したテストヘルパーを使って、テストを書きましょう。モックサーバに対する*redis.Clientを用意してそれをテスト対象の関数に渡せば完了です。

persistence/kvs/client_test.go
package kvs_test

import (
    "testing"
    "time"

    "github.com/higasgt/go-snippets/redis-cli/persistence/kvs"
    "github.com/higasgt/go-snippets/redis-cli/testhelper"
)

func TestSetToken(t *testing.T) {
    client := testhelper.NewMockRedis(t)

    if err := kvs.SetToken(client, "test", 1); err != nil {
        t.Fatalf("unexpected error while SetToken '%#v'", err)
    }
    actual, err := client.Get("TOKEN_test").Result()
    if err != nil {
        t.Fatalf("unexpected error while get value '%#v'", err)
    }

    if expected := "1"; expected != actual {
        t.Errorf("expected value '%s', actual value '%s'", expected, actual)
    }
}

func TestGetIDByToken(t *testing.T) {
    client := testhelper.NewMockRedis(t)
    if err := client.Set("TOKEN_test", 1, time.Second*1000).Err(); err != nil {
        t.Fatalf("unexpected error while set test data '%#v'", err)
    }

    actual, err := kvs.GetIDByToken(client, "test")
    if err != nil {
        t.Fatalf("unexpected error while GetIDByToken '%#v'", err)
    }
    if expected := 1; expected != actual {
        t.Errorf("expected value '%#v', actual value '%#v'", expected, actual)
    }
}

これで、Redisを扱っているコードのテストを書くことができました。

おわりに

今回は、redisを扱うコード部分の実装を行い、それに対してテストを書く方法を紹介しました。

明日は、@takochuu さんです!

標準パッケージから見るパッケージ構成のパターンの解説

こんにちは!これはGo3 Advent Calendar 2018、2日目の記事です。
1日目は、higasgtさんのredisを扱うコードをユニットテストするでした。

TL;DR

  • Goのパッケージ構成は、標準パッケージでも複数パターンの構成が存在している
  • 習う際は、いくつかのパッケージ構造を比較してどのように実装するかを決めるのが良い
  • io と database/sql は有名な標準パッケージだがパッケージ構成のアプローチは異なる

ioパッケージの構成について

ioパッケージは io.Reader io.Writer をはじめとして有名なパッケージです。
Goを触っている人は ioutil.ReadAll() にお世話になった人も多いのではないでしょうか。

io
├── example_test.go
├── io.go
├── io_test.go
├── ioutil
│   ├── example_test.go
│   ├── ioutil.go
│   ├── ioutil_test.go
│   ├── tempfile.go
│   ├── tempfile_test.go
│   └── testdata
│       └── hello
├── multi.go
├── multi_test.go
├── pipe.go
└── pipe_test.go

ioパッケージを構成するディレクトリ構造は上記のようになっています。
1つづつ解説していきます。

io.go

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

io.go は Reader Writer Closer をはじめとする抽象を実装しています。
パッケージのコメントに書いてある通りです。

Package io provides basic interfaces to I/O primitives.
Its primary job is to wrap existing implementations of such primitives, such as those in package os, into shared public interfaces that abstract the functionality, plus some other related primitives.

ioutil/ioutil.go

ioutilパッケージについては、下記のコメントのようにいくつかのutilityを実装しています。

Package ioutil implements some I/O utility functions.

具体的な実装として呼び出す際は下記のように ioutil の具象実装を利用します

r := strings.NewReader("Go is a general-purpose language designed with systems programming in mind.")

b, err := ioutil.ReadAll(r)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("%s", b)

なので、パッケージ構成としては下記のように、ioが抽象 / io/ioutilが具象となっています。

io // 抽象
├── example_test.go
├── io.go
├── io_test.go
├── ioutil // 具象
│   ├── example_test.go
│   ├── ioutil.go
│   ├── ioutil_test.go
│   ├── tempfile.go
│   ├── tempfile_test.go
│   └── testdata
│       └── hello
├── multi.go
├── multi_test.go
├── pipe.go
└── pipe_test.go

database/sql

database/sqlパッケージについてもioの時と同様に追いかけていきましょう。

sql/
├── convert.go
├── convert_test.go
├── ctxutil.go
├── doc.txt
├── driver
│   ├── driver.go
│   ├── types.go
│   └── types_test.go
├── example_test.go
├── fakedb_test.go
├── sql.go
└── sql_test.go

パッケージ構成は上記のようになっています。

sql.go

func Open(driverName, dataSourceName string) (*DB, error) {
    driversMu.RLock()
    driveri, ok := drivers[driverName]
    driversMu.RUnlock()
    if !ok {
        return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
    }

    if driverCtx, ok := driveri.(driver.DriverContext); ok {
        connector, err := driverCtx.OpenConnector(dataSourceName)
        if err != nil {
            return nil, err
        }
        return OpenDB(connector), nil
    }

    return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

sql.goは上記のように、 driveri, ok := drivers[driverName] という行で Driver を取り出して処理します。
Driver は ioの時とは逆に、driver ディレクトリの中に実装されており、 Driver インターフェースなど抽象実装がされています

type Driver interface {
    // Open returns a new connection to the database.
    // The name is a string in a driver-specific format.
    //
    // Open may return a cached connection (one previously
    // closed), but doing so is unnecessary; the sql package
    // maintains a pool of idle connections for efficient re-use.
    //
    // The returned connection is only used by one goroutine at a
    // time.
    Open(name string) (Conn, error)
}

go-sql-driver/driver.go

このDriverを使用しているのが go-sql-driverパッケージです。
以下のように init() 関数でDriverを登録し、その後の処理で利用しています。

func init() {
    sql.Register("mysql", &MySQLDriver{})
}

パッケージの構成がioとは逆で、以下のようになります。

sql/ // 具象
├── convert.go
├── convert_test.go
├── ctxutil.go
├── doc.txt
├── driver // 抽象
│   ├── driver.go
│   ├── types.go
│   └── types_test.go
├── example_test.go
├── fakedb_test.go
├── sql.go
└── sql_test.go

まとめ

  • パッケージの構成は標準パッケージでも異なる
  • 自分の実装の参考にする際は、複数のパッケージ構成を見比べた上でメリットがある方をえらぶとよい

おわりに

今日は標準パッケージの読み方を紹介しました。
明日は m_green14 さんです!

Goで簡易なプロトコルの実装をしてみた

こちらは、Go3 Advent Calendar 2018 の 3 日目の記事です。
昨日は、@takochuu さんの 標準パッケージから見るパッケージ構成のパターンの解説でした。
(標準パッケージでも、様々な構成があって勉強になりました。)

概要

Go で非常に簡単なプロトコルを実装してみました。
(本当に簡単なのでご容赦ください。)

対象

  • プロトコルってどうやって作るのか気になる方
  • なんとなく読んでみたい方

内容

  • 簡単なプロトコルの Go 実装
    • アプリケーション層のプロトコルを実装
    • ネットワーク層は TCP 通信

詳細

プロトコル仕様

簡単なユーザー認証が入ったプロトコルで、下記の通りに通信します。

1. 初期リクエストパケット (Clinet -> Server)

対象リクエストか否かを判定するため、Clientから特定のbyte(ENQ)のリクエストを送ります。

|-----------------------------|
| 1byte | リクエスト通知 (0x05) |
|-----------------------------|
2. 初期レスポンスパケット (Server -> Client)

対象の場合は、認証のための情報を返します。
今回はパスワードハッシュ化のためのキーを返しています。

|-----------------------------------------|
| 1byte  | リクエスト結果(OK=0x06, NG=0x06) |
| 3byte  | バージョン ex)1.0.0             |
| 10byte | リクエストID                    |
| 20byte | パスワードハッシュ化キー           |
|-----------------------------------------|
3. 認証リクエストパケット (Client -> Server)

ユーザーIDとパスワードを送信します。パスワードは先程受け取ったハッシュ化キーを利用してハッシュ化します。
(※ ユーザーIDは10byte以上のデータは利用できないです)

|------------------------------|
| 3byte  | バージョン            |
| 10byte | リクエストID          |
| 10byte | ユーザーID            |
| 32byte | ハッシュ化済みパスワード |
|------------------------------|
4. 認証レスポンスパケット (Server -> Client)

受信したユーザーID/パスワードから、結果を返します。

|------------------------------------|
| 3byte  | バージョン                  |
| 10byte | リクエストID                |
| 1byte  | 認証結果(OK=0x06, NG=0x06) |
|------------------------------------|
5. データ送/受信

(※ ここからは、まだ未実装かつプロトコル未定)

実装

GitHub上に上げています。
midorigreen/groto

├── example         # サンプル
│   ├── client
│   │   └── client.go
│   └── server
│       └── server.go
├── README.md
├── go.mod
├── groto.go        # protocol定義
├── client.go       # protocol client実装
└── server.go       # protocol server実装

使い方

groto/example

プロトコルの利用方法としては、 net.Conn interfaceの実装を渡すだけで、認証まで実行するようにしています。

Client

// ユーザー/パスワードを渡してClient生成
cli := groto.NewClient(user, password)
// 初期→認証まで実行
if err := cli.Do(conn); err != nil {
  return err
}

Server

l, err := net.Listen("tcp", ":8080")
if err != nil {
  return err
}
for {
  conn, err := l.Accept()
  if err != nil {
    log.Println(err)
  }
  go func(conn net.Conn) {
    defer conn.Close()
    // Server生成
    s := groto.NewServer()
    // 初期→認証まで実行
    if err := s.Do(conn); err != nil {
      return
    }

    // やりたい処理
    for {
      b := make([]byte, 2*1024)
      _, err := conn.Read(b)
    }
  }(conn)
}

(書きながら気づいたんですが、このあたりの待受の処理等もプロトコル側の実装で持たせればよかったかなと思います。)

追記: ループ時にconnが上書きされる問題を修正しています

プロトコル側

基本的に各送信ごとのパケットをstructで定義しています。
例えば、2. 初期レスポンスパケットは下記の通りに定義しています。

type PacketHandshake struct {
  status    Status
  version   []byte
  id        []byte
  pwHashKey []byte
}

パケット間通信はbyte配列をやりとりするため、byteへの変換処理が必要になります。struct to byteをMarshal、byte to structをUnmarshalとしてメソッドを定義します。愚直な実装をしてます。

func (i *PacketHandshake) Marshal() []byte {
  b := make([]byte, 0, initLen)

  b = append(b, byte(i.status))
  b = append(b, i.version...)
  b = append(b, i.id...)
  b = append(b, i.pwHashKey...)

  return b
}

func UnmarshalHandshake(b []byte) (PacketHandshake, error) {
  return PacketHandshake{
    status:    Status(b[0]),
    version:   b[1:4],
    id:        b[4:14],
    pwHashKey: b[14:initLen],
  }, nil
}

各パケットを上記のように定義して、後は決められた通りにパケットをやり取りする実装を書きました。
パケットの交換は net.Conn interfaceのRead/Writeメソッドを利用して実装しています。

全体像

サーバー側

type Server struct {
  hashKey []byte
}

func NewServer() *Server {
  return &Server{}
}

func (s *Server) Do(conn net.Conn) error {
  // 初期パケットの処理
  if err := s.stepHandshake(conn); err != nil {
    return err
  }
  // 認証パケットの処理
  if err := s.stepAuthN(conn); err != nil {
    return err
  }
  return nil
}

クライアント側

type Client struct {
  user      string
  password  string
  id        []byte
  pwHashKey []byte
}

func NewClient(user, password string) *Client {
  return &Client{
    user:     user,
    password: password,
  }
}

func (c *Client) Do(conn net.Conn) error {
  // 初期パケット処理
  if err := c.stepHandshake(conn); err != nil {
    return err
  }
  // 認証パケット処理
  if err := c.stepAuthN(conn); err != nil {
    return err
  }
  return nil
}

各パケット実装

パケット送受信を実装しているメソッドは下記で、net.Conn を引数に取ります。

// Client側 メソッド
func (c *Client) stepHandshake(conn net.Conn) error {}

// Server側メソッド
func (s *Server) stepHandshake(conn net.Conn) error {}

パケットの流れベースで見ると、処理は下記の通りになっています。

例) 初期パケット

① Client (初期リクエストパケット送信)

// conn = net.Connを実装したstruct
// 今回は *TCPConn

// 初期パケット送信
_, err := conn.Write([]byte{0x05})
if err != nil {
  return err
}


② Server (初期リクエストパケット受信)

b := make([]byte, 2*1024)
// 初期パケットを読み込み
_, err := conn.Read(b)
if err != nil {
  return fmt.Errorf("failed read connection: %v", err)
}

// 対象パケットか否かを判定
if b[0] != 0x05 {
  // エラー処理
}
// 結果を返すパケットのstructを作成
i, err := NewPacketHandshake(OK)
if err != nil {
  return fmt.Errorf("failed create init proto: %v", err)
}
// 結果をconnectionへ書き込み
_, err = conn.Write(i.Marshal())
if err != nil {
  return err
}


③ Client

b := make([]byte, 33)
// 初期リクエスト結果を読み込み
_, err = conn.Read(b)
if err != nil {
  return err
}
// structへ変換
i, err := UnmarshalHandshake(b)
if err != nil {
  return err
}
// 結果を確認
if i.status != OK {
  return errors.New("failed init")
}

このような処理を、各ステップごとにClient/Server側で実装しています。
一応、1~4の仕様を満たした実装が完了しています。

まとめ

非常に簡易で実用性はほぼなさそうですが、プロトコル実装してみました。
このプロトコルを拡張して、作ろうとしているツールの初期認証かつその後のデータのやり取りに利用しようと考えています。
作る流れとしては、プロトコル仕様を作ってから実装の想定でしたが、
とりあえず動くものが見たくなったため、ソケット通信を先に実装して動かしながら作りました。
ソケット通信部分は、こちらを参考しました。(書籍版)
GoでたたくTCPソケット(前編)|Goならわかるシステムプログラミング

おわりに

ここまで読んでいただきありがとうございました。
明日は、@r-fujimotoさんです。

参考

Goでサーバー開発するときのMakefileを晒してみる

はじめに

この記事は、Go3 Advent Calendar の4日目の記事です。

Goで開発する際にはテストの実行やlintの実施といった細々としたコマンドを Makefile にまとめることが多いと思います。
これにはコマンド入力の手間を省くのももちろんですが、チーム内でコマンド実行の方法を統一するという意味もあります。「手元でのテストはReadmeに書いてある通りに実行してね」と伝えるよりも、Makefileにまとまってる方が親切です。

ということで、何番煎じか分かりませんが今回は業務で使っている Makefile を晒してみたいと思います。

ちなみに主に以下のツールを利用しています。

  • パッケージ管理: dep
  • 自動リロード: realize
  • DB migration管理: goose

Makefile

以下の物を使用しています。

setup:
    go get -u github.com/golang/dep/cmd/dep
    go get -u golang.org/x/lint/golint
    go get -u golang.org/x/tools/cmd/goimports
    go get -u bitbucket.org/liamstask/goose/cmd/goose
    go get -u github.com/oxequa/realize
    go get -u github.com/motemen/gore
    go get -u github.com/golang/mock/gomock
    go get -u github.com/golang/mock/mockgen
dep:
    dep ensure
lint:
    go tool vet app
    golint -set_exit_status app/...
fmt: lint
    goimports -w app
build: fmt lint
    go build -o dist/server app/server.go
run:
    realize start -n app-server
test:
    env ENV=test go test -cover -race -count=1 ./app/...
goose:
    goose up
    goose -env test up
setup-db: goose
    bin/mysql -T < db/dummy.sql

軽く解説

setup

setupでは開発に必要なcliツールのインストールをまとめてやっています。
depではこういった実行ツール系の依存を解決できないため、このプロジェクトで使うツールの一覧をまとめるという意味もあります。

fmt + lint

lint コマンドではgo vetとgolintによるコード解析をまとめて実施しています。分けて実施する意味もあまりないでしょうし。
fmtではgoimportsによるコードのformattingをしています。最初にlintを実施しているため、普段はmake fmtとやってlintingもformattingもまとめて実施しています。

test

testは何気に環境の指定やオプション指定などで暗黙知が生まれやすいコマンドだと思っています。
-raceなどは当然ciでもチェックしていますが、push前に手元でも見つけられるようにつけています。

ただしこのmake testコマンドはpush前の最終確認などで使うことが多く、開発中はvscodeやIDEの機能を使ってファイル単位でテストを走らせることが多いです。そのためどちらかというと利便性よりも網羅性側に切り倒して作っています。

run

開発中のhot reloadのためにrealizeを導入しています。ただし若干使い方が面倒なため、上のsetupでrealizeを導入しmake runコマンドで勝手に実行することで、新人さんなどがrealizeの存在を意識せず便利な開発環境が使えるようにしています。

終わりに

今回はプロジェクトで使用しているMakefileを晒してみました。
開発中に実行するコマンド類は暗黙知になりやすいため、出来るだけそのようなことが無いようにしていきたいと思います。

Go標準のflagパッケージと比べてみようサードパーティflagライブラリ

tl;dr

GoでCLIのツールを作るとき、皆さんフラグをどう実装しますか?

標準のflagパッケージでシンプルにしますか?
それともCLIコマンドライブラリで複雑でリッチなモダンコマンドにしますか?

今回はいくつもあるCLIコマンドライブラリから、フラグを制御するライブラリに着目しようと思います。

まず手始めに標準のflagパッケージを見た上で、各ライブラリは何を解決したいのか。どのような方法で解決しようとしているのか。
この機会にちょっと眺めて遊んでみようと思います。

Go標準flagパッケージ

Go製のCLIツールを見ていくと、どうもサードパーティライブラリを使っている方が多く印象を受けます。
このコマンド、中身どうなっているかな。
と見に行くと、ほとんどのツールは標準flagパッケージではなく、サードパーティライブラリをつかっています。
Go公式ツールや、古くに作られたツールなどは標準のflagパッケージを使っていますね。

Go的にはやっぱり標準よ!という方が多い中で
現状こうなっていることを考えると、標準のflagパッケージには足りない部分があり、
足りない部分を補うため、ライブラリを使っているのではないでしょうか。

では具体的にflagパッケージではなにが出来てなにが出来ないのか、見ていきましょう。

オプションスタイル

flagパッケージを使っているGo公式コマンドを見ていきましょう。
以下はgofmtのhelpです。

usage: gofmt [flags] [path ...]
  -cpuprofile string
        write cpu profile to this file
  -d    display diffs instead of rewriting files
  -e    report all errors (not just the first 10 on different lines)
  -l    list files whose formatting differs from gofmt's
  -r string
        rewrite rule (e.g., 'a[b:len(a)] -> a[b:]')
  -s    simplify code
  -w    write result to (source) file instead of stdout

ふむ、なかなかにシンプルで質実剛健といった感じですね。
参考にGNU grepのヘルプと比べてみましょう。

Usage: grep [OPTION]... PATTERN [FILE]...
Search for PATTERN in each FILE.
Example: grep -i 'hello world' menu.h main.c

Pattern selection and interpretation:
  -E, --extended-regexp     PATTERN is an extended regular expression
  -F, --fixed-strings       PATTERN is a set of newline-separated strings
  -G, --basic-regexp        PATTERN is a basic regular expression (default)
  -P, --perl-regexp         PATTERN is a Perl regular expression
  -e, --regexp=PATTERN      use PATTERN for matching
  -f, --file=FILE           obtain PATTERN from FILE
  -i, --ignore-case         ignore case distinctions
  -w, --word-regexp         force PATTERN to match only whole words
  -x, --line-regexp         force PATTERN to match only whole lines
  -z, --null-data           a data line ends in 0 byte, not newline

Miscellaneous:
  -s, --no-messages         suppress error messages
  -v, --invert-match        select non-matching lines
  -V, --version             display version information and exit
      --help                display this help text and exit
...

割愛

ザラッと見てみると、grepのほうがしっかりした印象を受けます。
しかしその中でもとりわけ大きな差があるのにお気づきになったでしょうか?

そうロングスタイルオプションとショートスタイルオプションが分かれていないのです。

解説すると、
ショートオプションが-から始まり、一文字の英字から構成されるオプションです。-fとか
ロングスタイルオプションが--から始まり、複数の英字から構成されるオプションです。--fileとか
GNUコマンドが使っているオプション一覧があるので見てみるとわかりやすいし楽しいです。

Table of Long Options

話を戻しますがgofmtの-cpuprofile-(ハイフン)一つなのに複数英字構成となっていますよね。
つまりflagパッケージは、GNU/POSIXのコマンドラインスタイルとは異なる形式となっているわけです。

標準flagパッケージの使用方法

次はgofmtのソースコードを少し覗いてみましょう。
gofmtのソースはgolang/gosrc/cmd/gofmt配下にあります

以下はgofmtのフラグ定義となります。

var (
    // main operation modes
    list        = flag.Bool("l", false, "list files whose formatting differs from gofmt's")
    write       = flag.Bool("w", false, "write result to (source) file instead of stdout")
    rewriteRule = flag.String("r", "", "rewrite rule (e.g., 'a[b:len(a)] -> a[b:]')")
    simplifyAST = flag.Bool("s", false, "simplify code")
    doDiff      = flag.Bool("d", false, "display diffs instead of rewriting files")
    allErrors   = flag.Bool("e", false, "report all errors (not just the first 10 on different lines)")

    // debugging
    cpuprofile = flag.String("cpuprofile", "", "write cpu profile to this file")
)

なおusageの出力は以下のようになっていました。ヘルプ表示時に以下がコールされているわけですね。

func usage() {
    fmt.Fprintf(os.Stderr, "usage: gofmt [flags] [path ...]\n")
    flag.PrintDefaults()
}

フラグの定義関数の中からflag.Boolの定義を見てみると、
フラグを構成する定義する項目がシンプルであることがわかります。

func Bool(name string, value bool, usage string) *bool

引数を見ると以下の3つからフラグが構築されていることになります

  • フラグ名
  • デフォルト値
  • ヘルプメッセージとして表示する使用方法

ロングオプションとショートオプションを、分けて入力するようにはなっていませんね。

試しに以下のように-を余分につけてみたらそれっぽくなったので、
ロングオプションとショートオプションを定義してどちらかを使用するように実装すればできなくはないですね。
(じつはもっと綺麗なやり方あるぞという方は教えていただければ...)

l           = flag.Bool("l", false, "list shorthand")
list        = flag.Bool("-list", false, "list files whose formatting differs from gofmt's")

しかし、これはあまり筋がいいとは言えませんね...

サードパーティライブラリ

上記で解説したとおり、flagパッケージはPOSIX/GNU-styleとして定義されたコマンドラインインタフェースの標準とは
ことなるフラグのデザインになるわけです。
よって多くのサードパーティライブラリは、POSIX/GNU-styleに準拠するために、
flagの再実装を行うという目的が共通項としてあるように感じます。

実際、サードパーティライブラリの一つであるpflagのREADMEには、

Package pflag is a drop-in replacement for Go's flag package, implementing POSIX/GNU-style --flags.
pflag is compatible with the GNU extensions to the POSIX recommendations for command-line options.
See http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html

POSIX/GNU-styleのオプションのGo実装であり、このスタイルに互換性があると書いてあります。

さて、それではサードパーティ製のライブラリがどのようにフラグを定義するのかを見てみましょう。

urfave/cli

  • シンプルかつ強力なコマンドラインツールライブラリ
  • フラグはそれぞれの型で用意されたstructを用いて定義

GoでCLIのライブラリといえばまずurfave/cliは外せませんね。
RubyならRails、PythonならDjango、PHPならLaravel、GoでCLI作るならurfave/cliですよ。
サブコマンドを実装するためという文脈で語られることが多いですが、
フラグ制御の仕方も、シンプルかつ強力な感じでGoodです。

実装例を見てみましょう(READMEから転載)

package main

import (
  "fmt"
  "log"
  "os"

  "github.com/urfave/cli"
)

func main() {
  app := cli.NewApp()
  app.Name = "greet"
  app.Usage = "fight the loneliness!"
  app.Action = func(c *cli.Context) error {
    fmt.Println("Hello friend!")
    return nil
  }

  err := app.Run(os.Args)
  if err != nil {
    log.Fatal(err)
  }
}

なんと、たったこれだけでコマンドの雛形が出来てしまいました。素晴らしい。
ヘルプメッセージは以下のような感じです。

$ greet help
NAME:
    greet - fight the loneliness!

USAGE:
    greet [global options] command [command options] [arguments...]

VERSION:
    0.0.0

COMMANDS:
    help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS
    --version Shows version information

さて肝心のフラグについてです。
urfave/cliではFlagは型毎にstructが用意されていおり、基本的にそこにフラグ定義を入れていき、
本体であるAppに設定するという流れです。
例えばhogeというフラグを設定するなら以下のような形ですね

    var hogeFlag string
    app.Flags = []cli.Flag{
        cli.StringFlag{
            Name:        "hoge, H",
            Usage:       "hoge hoge hoge",
            EnvVar:      "HOGE",
            Hidden:      false,
            Value:       "defoge",
            Destination: &hogeDestination,
        },
    }

環境変数をデフォルト値として割当するEnvVar
隠しフラグとしてヘルプに表示しないHidden
変数にバインドするDestinationがにくいですね
Destinationなしでも、以下のようにフラグ名を指定して取得もできます。

    hogeFlag := c.String("hoge")

上記設定したフラグをヘルプ表示すると以下のようになります。

   --hoge value, -H value           hoge hoge hoge (default: "defoge") [$HOGE]

私は使ったことがないですが、bash completionもサポートしてるようです。

mow.cli

urfave/cliを意識して、よりベターなコマンドラインライブラリとして作成されたmow.cliなどもあります。

公式のREADMEにはurfave/cliとの比較表などあり、
より柔軟なフラグの組み合わせやArgsの制御をウリにしているようです。

go-flags

今回紹介している他のライブラリが変数にバインディングしたり、
単一の値を返す形式なのに対して、go-flags
フラグの集まりをstructとして定義し、そのstructの各変数に対してフラグの値をマッピングするのが特徴です。
structに対してフラグ用のタグを付加することで細やかな表示をするが出来ます。

package main

import (
    "bytes"
    "fmt"

    flags "github.com/jessevdk/go-flags"
)

type CreateOption struct {
    Title   string `short:"i" long:"title" value-name:"<title>" description:"The title of an issue"`
    Message string `short:"m" long:"message" value-name:"<message>" description:"The message of an issue"`
}

type ListOption struct {
    Num     int    `short:"n" long:"num" value-name:"<num>" default:"20" default-mask:"20" description:"Limit the number of issue to output."`
    State   string `long:"state" value-name:"<state>" default:"all" default-mask:"all" description:"Print only issue of the state just those that are \"opened\", \"closed\" or \"all\""`
    Scope   string `long:"scope" value-name:"<scope>" default:"all" default-mask:"all" description:"Print only given scope. \"created-by-me\", \"assigned-to-me\" or \"all\"."`
    OrderBy string `long:"orderby" value-name:"<orderby>" default:"updated_at" default-mask:"updated_at" description:"Print issue ordered by \"created_at\" or \"updated_at\" fields."`
    Sort    string `long:"sort"  value-name:"<sort>" default:"desc" default-mask:"desc" description:"Print issue ordered in \"asc\" or \"desc\" order."`
    Search  string `short:"s" long:"search"  value-name:"<search word>" description:"Search issues against their title and description."`
}

type Option struct {
    CreateOption *CreateOption `group:"Create Options"`
    ListOption   *ListOption   `group:"List Options"`
}

func newOptionParser(opt *Option) *flags.Parser {
    opt.CreateOption = &CreateOption{}
    opt.ListOption = &ListOption{}
    parser := flags.NewParser(opt, flags.HelpFlag|flags.PassDoubleDash)
    parser.Usage = `issue - Create and Edit, list a issue

Synopsis:
  # List issue
  lab issue [-n <num>] [--state=<state> | -o | -c] [--scope=<scope> | -r | -a] [-s]
            [--orderby=<orderby>] [--sort=<sort>] [-A]

  # Create issue
  lab issue [-e] [-i <title>] [-m <message>] [--assignee-id=<assignee id>]

  # Show issue
  lab issue <issue iid> [--no-comment]`
    return parser
}

func main() {
    buf := &bytes.Buffer{}
    var opt Option
    parser := newOptionParser(&opt)
    parser.WriteHelp(buf)
    fmt.Println(buf.String())
}

ヘルプメッセージは以下のようになっています。
struct毎にフラグをグルーピングしたり、その構造をヘルプに反映したりできます。

Usage:
  goflags issue - Create and Edit, list a issue

Synopsis:
  # List issue
  lab issue [-n <num>] [--state=<state> | -o | -c] [--scope=<scope> | -r | -a] [-s]
            [--orderby=<orderby>] [--sort=<sort>] [-A]

  # Create issue
  lab issue [-e] [-i <title>] [-m <message>] [--assignee-id=<assignee id>]

  # Show issue
  lab issue <issue iid> [--no-comment]

Create Options:
  -i, --title=<title>           The title of an issue
  -m, --message=<message>       The message of an issue

List Options:
  -n, --num=<num>               Limit the number of issue to output. (default: 20)
      --state=<state>           Print only issue of the state just those that are "opened", "closed" or "all" (default: all)
      --scope=<scope>           Print only given scope. "created-by-me", "assigned-to-me" or "all". (default: all)
      --orderby=<orderby>       Print issue ordered by "created_at" or "updated_at" fields. (default: updated_at)
      --sort=<sort>             Print issue ordered in "asc" or "desc" order. (default: desc)
  -s, --search=<search word>    Search issues against their title and description.

サンプルがやけに具体的なのは、私が作っているlabというコマンドで使ってるからです。
GitLabのCLIクライアントです。GitLabを使っているのであれば使ってみてください。

pflag

  • モダンなコマンドラインツールライブラリcobraで使用されているフラグライブラリ
  • 標準のflagパッケージを強く意識した使用感

cobraと一緒にくっついてくるフラグライブラリpflagです。
cobraといえば、Dockerでも使われているモダンなコマンドライブラリとして有名ですね。

pflagはベターflagライブラリとして、標準を差し替えるような形です。使用時のパッケージ名もflagですしね
使用感としては、flagと似たような形になっています。

ざっくり使うとこうなる

package main

import (
    "fmt"

    flag "github.com/spf13/pflag"
)

var helpFlag bool
var fooFlag string
var barFlag string

func main() {
    flag.BoolVarP(&helpFlag, "help", "p", false, "show help message")
    flag.StringVar(&fooFlag, "foo", "defoo", "help message foo")
    flag.StringVarP(&barFlag, "bar", "b", "debar", "help message bar")
    flag.Parse()

    if helpFlag {
        flag.PrintDefaults()
        return
    }

    fmt.Println(fooFlag, barFlag)
}

ヘルプメッセージはこんな感じです。

  -b, --bar string   help message bar (default "debar")
      --foo string   help message foo (default "defoo")
  -p, --help         show help message

また変数にフラグ値をバインディングせずに、そのまま値を返却することも可能です。
これは上記と同じ意味になります。

package main

import (
    "fmt"

    flag "github.com/spf13/pflag"
)

var (
    helpFlag = flag.BoolP("help", "p", false, "show help message")
    fooFlag  = flag.String("foo", "defoo", "help message foo")
    barFlag  = flag.StringP("bar", "b", "debar", "help message bar")
)

func main() {
    flag.Parse()

    if *helpFlag {
        flag.PrintDefaults()
        return
    }

    fmt.Println(*fooFlag, *barFlag)
}

ご覧の通り基本的な使用感は標準のflagパッケージと同様となります。
ここで注目したいのはfunc StringVarP(p *string, name, shorthand string, value string, usage string)などの型P形式の関数です。
flagパッケージの関数にshorthandつまりショートオプション用の引数が追加されています。

他にも非推奨フラグに対して警告を出す機能など運用に役立つような機能などがあります。

Kingpin

  • メソッドチェーンによるフラグの属性付加が特徴的
  • なんかいろいろな形式の文字列をパースしたりできる模様

この記事を書くまで以前は触ったことがなかったのですが、
今回触ってみて意欲的なコンセプトなライブラリだと感じました。
メソッドチェーンによってフラグの機能を追加していく。とてもおもしろいですね。
2017年末以降にCommitが止まっているのが残念です。

フラグの設定は以下のようになります

package main

import (
  "fmt"

  "gopkg.in/alecthomas/kingpin.v2"
)

var (
  debug   = kingpin.Flag("debug", "Enable debug mode.").Bool()
  timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").OverrideDefaultFromEnvar("PING_TIMEOUT").Short('t').Duration()
  ip      = kingpin.Arg("ip", "IP address to ping.").Required().IP()
  count   = kingpin.Arg("count", "Number of packets to send").Int()
)

func main() {
  kingpin.Version("0.0.1")
  kingpin.Parse()
  fmt.Printf("Would ping: %s with timeout %s and count %d\n", *ip, *timeout, *count)
}

ヘルプメッセージは以下のようになります。

usage: kingpin [<flags>] <ip> [<count>]

Flags:
      --help        Show context-sensitive help (also try --help-long and --help-man).
      --debug       Enable debug mode.
  -t, --timeout=5s  Timeout waiting for ping.
      --version     Show application version.

Args:
  <ip>       IP address to ping.
  [<count>]  Number of packets to send

他ライブラリではパースを実施し、その後に引数を引き渡して後はよしなに。
といった形式が多いのですが、kingpinは引数の形式も指定することができます。

さらに多数の便利機能が魅力で、以下のような特定の文字列をパースまで対応することができます。

  • IP(1.1.1.1)
  • TCP(1.1.1.1:3333)
  • File(実在するファイル)
  • Enum(特定の文字列)

flaggy

  • フラグライブラリとしては後発
  • シンプルで余分な機能がないフラグとしての機能に注力

flaggyは割と後発のライブラリなのですが、
もろもろ便利にしようぜというフラグライブラリとは逆に、余分な機能を削ぎ落としたシンプル・イズ・ベスト方面です。
以下のようなシンプル構造。

var stringFlag = "defaultValue"
flaggy.String(&stringFlag, "f", "flag", "A test string flag")

フラグ定義関数の中にデフォルト値がないのが印象的ですね。

func String(assignmentVar *string, shortName string, longName string, description string)

まとめ

まったく。フラグは奥がふかいなぁ。
一回見始めたら中身のコードまで気になってしまってあまり文量が書けなかった...反省

Go + Google Cloud Pub/Sub で GAE SE (gen1) から Publish するときの注意点

Go3 Advent Calendar 2018 6日目を担当する avvmoto です。よろしくお願いします。

この記事では、Go (gen 1) on Google App Engine SE (GAE SE) から Google Cloud Pub/Sub で Publish するときの注意点を解説します。
なお、go 1.11 以降の gen2 はこの記事の対象外です。

はじめに

GAE SE は Web Application の作成に適した PaaS です。GAE から分析用途等で、 Pub/Sub へ message を Publish したいケースは珍しくないです。
これを実現するときの注意点をご紹介します。

cloud.google.com/go/pubsub の課題

Cloud Pub/Sub の公式ドキュメント をみると、冒頭で紹介されているクライアントライブラリー cloud.google.com/go/pubsub を使いたくなります。ただしこれは、GAE SE から使うには以下のような課題があります。実際、このドキュメントの中程には、GAE SE からだと別のライブラリを使うよう書いてあります。

If you're running on Google App Engine standard, we recommend using the older Google APIs Client Libraries.

課題1: latancy が大きい

GAE SE の handler で、1request の中で client を作成し、 Publish する例を考えてみましょう。実装としては以下のようになります。

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := appengine.NewContext(r)
    client, err := pubsub.NewClient(ctx, appengine.AppID(ctx))
    if err != nil {
        return // todo
    }
    defer client.Close()

    topic := client.Topic("eventlog-view")
    defer topic.Stop()
    result := topic.Publish(ctx, &pubsub.Message{
        Data: []byte("hello world"),
    })

    id, err := result.Get(ctx)
    if err != nil {
        return // todo
    }
    log.Infof(ctx, "Published a message with a message ID: %s\n", id)
}

この実装だと latency が大きくなってしまいます。 message のサイズにもよると思いますが、筆者が実験したところ 100 ms から 300 ms かかりました。
これは Web app 中に非同期ではなく行う処理としては遅いです。

ただ、godoc によるとそもそもこのような使い方は想定されていないようです。

Clients should be reused rather than being created as needed. A Client may be shared by multiple goroutines. 1

godoc によると、このように client は再利用し、複数のgoroutine から共有される使い方を想定されています。

If the client is available for the lifetime of the program, then Close need not be called at exit. 2

またこのような記述もあって、client はプログラムのライフサイクルを通じてずっと起動することも想定されているようです。

1request の中で client を作成し Publish し、client を close といったユースケースでは作られていないようで、むしろずっとclient を起動しっぱなしにする想定のようです。
実際、上記コードを動かしてみると、Stackdriver Trace でみると一回のPublishで複数回通信が走っています。一回Publishしてcloseという用途には最適化されていないような印象を受けます。

image.png

課題2: Socket API の quota を消費する

cloud.google.com/go/pubsub は Socket API 経由で RPC を叩いているようで 3、Socket API 関連の quota Sockets created per day を消費します。
これはコンソール https://console.cloud.google.com/iam-admin/quotas から確認できます。 Socket API の quota は、GCP の他の quota と比較して少なめに設定されており、注意していないと quota limit に引っかかってしまいます。

依頼フローを経れば quota の上限を引き上げられますが、その後も quota の limit に引っかからないか、継続的にチェックする必要が出てきます。

解決策

1. Socket API の quota を引き上げ、 cloud.google.com/go/pubsub を使う

quota を十分引き上げることで、とりあえず quota の上限に引っかかり Publish できなくなる問題は防ぐことができます。その場合は引き続き latancy が大きい問題が残ります。用途によっては許容できるかもしれませんが、許容できない場合は、 Task Queue を用いて非同期に処理しましょう。

second-hanei2.png

Push 型の TaskQueue で一件ずつ処理すると、前述のとおり、このライブラリだと効率的に扱えないです。 Pull 型の Taskqueue として、Cron である程度の件数を取得し、まとめて Publish する実装が効率的でよいです。効率的に処理することで、インスタンス台数が減り、課金額をへらすことができます。

2. GAE SE gen2 に移行し、cloud.google.com/go/pubsub を使う

GAE gen2 となれば、Socket API の quota を気にせず良くなます。 go1.11 で cloud.google.com/go/pubsub を使いましょう。
ただし前述の latency の問題は引き続き考慮する必要があります。

3. google.golang.org/api/pubsub/v1 に乗り換える

Cloud Pub/Sub の公式ドキュメント にあるとおり、古い方のライブラリ、google.golang.org/api/pubsub/v1 を利用する方法です。こちらは内部的にREST APIを叩いているようで、Socket API のquota を消費せず、URLFetch の quota を消費します。

このライブラリの godoc には DEPRECATED と記載ある点は今後も利用を続ける点では一定リスクですが、公式ドキュメントで利用が奨励されている点は考慮にいれて良いと思います。

筆者が実験したところ、このライブラリだと概ね 100 ms 以下でpublish でき、特に非同期にすることなく利用することにしました。

このライブラリは、grpc から自動生成されたようで godoc も構成も分かりにくいですが、 Publish するものの実装するときにはこちらのサンプルが役に立ちました。
https://gist.github.com/broady/c79a65cb49d7b6a56448b3345a23b3d6

まとめ

Go on GAE SE (gen1) からカジュアルに cloud.google.com/go/pubsub を利用して Publish していると、レスポンスの悪化や、 Socket API の quota limit に気づかず障害、などの事態を引き起こす可能性があります。
事前にquota の limit を適切にあげてから利用するか、 REST API を叩く古いライブラリ google.golang.org/api/pubsub/v1 を利用しましょう。ただしこちらを利用するにしても、 URL Fetch の quota には注意が必要です( Socket API の quota よりシビアではないとはいえ)。また、公式ドキュメントから案内があるとはいえ、DEPRECATED となっている点も注意が必要です。

なおGAE SE gen2 になると状況が変わるので、別途最適な状況を考える必要があります。

謝辞

Pub/Sub 利用時の注意点を教えてくださった GCPUG の皆様、ありがとうございました。

参考資料

脚注


  1. https://godoc.org/cloud.google.com/go/pubsub#Client 

  2. https://godoc.org/cloud.google.com/go/pubsub#Client.Close 

  3. Package pubsub provides an easy way to publish and receive Google Cloud Pub/Sub messages, hiding the details of the underlying server RPCs. 4 

  4. https://godoc.org/cloud.google.com/go/pubsub 

Go でアプリケーションとクライアントのミドルウェアを作成する方法知ってますか?

世の中に沢山の「ミドルウェア」が存在しますが、ここで紹介するミドルウェアは、あるメインロジックを大きく変更することなく、その前後に挟む処理のことを指します。

アプリケーションを作成する場合に、メインロジックのハンドラを mainHandler として、ミドルウェア A, B, C を使用していた場合の挙動は以下の順序の通りになります。

request -> A -> B -> C -> mainHandler -> C -> B -> A -> response

これを意識するためには、Perl Monger にお馴染みの図を覚えるといいでしょう。玉ねぎ内部のそれぞれの層はミドルウェアを表現しています。


http://blog.nomadscafe.jp/2012/01/plackmiddlewareaccesslog.html

これを踏まえて Go でまずは、アプリケーションのミドルウェアを作成してみましょう。

アプリケーションミドルウェアを作成する

下記がアプリケーションサーバーのミドルウェアになります。ここでは app.ServeHTTP がアプリケーションのメインロジックとなり、その前後に何かしら処理を行う関数やメソッドを作成することが可能です。

type Middleware func(http.Handler) http.Handler

func Something() Middleware {
    return func(app http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 前処理
            Before()

            app.ServeHTTP(w, r)

            // 後処理
            After()
        })
    }
}

ここで前処理、後処理を意図させる関数を記述していますが、例としてこれらが行える処理はそれぞれ以下があるでしょう。

  • 前処理(この段階では唯一 request の情報を扱える)
    • 受け取った request の情報をロギングする。
    • もし、これから行う処理が panic() する場合に備えて recover() を挟んでおく
  • 後処理(この段階では request, response 両方の情報を扱える)

次にメインロジックを持つハンドラに、ミドルウェアを適用させるために下記の関数を作成します。この処理は始めに挙げた middleware の処理順序を保証するためのものです。

request -> A -> B -> C -> mainHandler -> C -> B -> A -> response

func UseMiddlewares(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

これらを踏まえて使い方は次の通りになります。

type Mux struct {
    mux *http.ServeMux
}

// ハンドラを登録する際にミドルウェアを挟む
func (m *Mux) Handle(pattern string, handler http.Handler) {
    mux.Handle(pattern, UseMiddlewares(handler,
        Something(),
        A(),
        B(),
        C(),
    ))
}

func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    m.mux.ServeHTTP(w, r)
}

func mainHandler() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, main logic")
        w.WriteHeader(http.StatusOK)
    })
}

// ハンドラを登録してサーブする
func RegisterHandlersAndServe() {
    server := new(http.Server)
    mux := &Mux{mux: http.NewServeMux()}

    mux.Handle("/hello", mainHandler())

    server.Handler = mux

    server.ListenAndServe() 
}

Go だと標準パッケージだけで、とても簡単にミドルウェアが実装できます。

ここまで紹介したミドルウェアの作成のノウハウは、アプリケーションのミドルウェアを作るためだけではなく、実は http クライアントのミドルウェアを作成する時にも役に立ちます。

クライアントのミドルウェアを作成する

同様にクライアントのためのミドルウェアを作成してみます。 net/http パッケージで提供されている http.RoundTripper interface を使用します。定義は下記のようになっています。

type RoundTripper interface {
    RoundTrip(*Request) (*Response, error)
}

RoundTripper は単一の HTTP トランザクションを扱うためのメインロジックを持ちます。そのため RoundTripper を実装する際に様々な注意点が存在します。
これらは GoDoc に記載されていますが、一応幾つか挙げると

  • Goroutine を用いた場合に concurrent safe になるように実装する
  • response を得られた場合は err == nil を返す
  • request の中身を弄ってはいけない

などです。気になる方は GoDoc を読んでください。

しかし、この RoundTripper を用いてクライアントのミドルウェアを作成することが可能です。まずは http.Handler と同様に以下を定義します。

// Middleware represents http client middleware
type Middleware func(http.RoundTripper) http.RoundTripper

// UseMiddlewares uses http client middlewares
func UseMiddlewares(r http.RoundTripper, middlewares ...Middleware) http.RoundTripper {
    for i := len(middlewares) - 1; i >= 0; i-- {
        r = middlewares[i](r)
    }
    return r
}

次に http.HandlerFunc に値する http.RoundTripper の型を定義してあげます。

// RoundTripperFunc represents http.RoundTripper
type RoundTripperFunc func(*http.Request) (*http.Response, error)

// RoundTrip do RoundTrip
func (f RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
    return f(r)
}

これらを定義しておくことによって、こんな感じに簡単に http.RoundTripper interface を満たした関数を定義することが可能になります。

func Something() Middleware {
    return func(next http.RoundTripper) http.RoundTripper {
        return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
            // 前処理
            Before()

            resp, err := next.RoundTrip(r)
            if err != nil {
                return nil, err
            }


            // 後処理
            After()
            return resp, nil
        })
    }
}

これらを用いて request と response のログを吐くようなミドルウェアを作成してみます。今回は、ロギングを行うパッケージとして有名な go.uber.org/zap を使用します。記述した Go のコードは以下の通りになりました。

// RequestLogging logs request contents.
func RequestLogging(logger *zap.Logger) Middleware {
    return func(next http.RoundTripper) http.RoundTripper {
        return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
            logger.Info("request logging",
                zap.String("RemoteAddr", r.RemoteAddr),
                zap.String("Content-Type", r.Header.Get("Content-Type")),
                zap.String("Path", r.URL.Path),
                zap.String("Query", r.URL.RawQuery),
                zap.String("Method", r.Method),
            )
            return next.RoundTrip(r)
        })
    }
}

// ResponseLogging logs response contents.
func ResponseLogging(logger *zap.Logger) Middleware {
    return func(next http.RoundTripper) http.RoundTripper {
        return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
            resp, err := next.RoundTrip(r)
            if err != nil {
                return nil, err
            }
            logger.Info("response logging",
                zap.Int("StatusCode", resp.StatusCode),
                zap.Int64("ContentLength", resp.ContentLength),
                zap.String("Path", r.URL.Path),
                zap.String("Query", r.URL.RawQuery),
                zap.String("Method", r.Method),
            )
            return resp, nil
        })
    }
}

ミドルウェアを幾つか定義したら *http.Client を作成する関数を定義します。これを用いて http の client を作成する事ができます。

func NewClient(logger *zap.Logger) *http.Client {
    return &http.Client{
        Transport: UseMiddlewares(
            http.DefaultTransport,
            RequestLogging(logger),
            ResponseLogging(logger),
        ),
    }
}

ちなみに http.DefaultTransporthttp.Transport の型を持ちます。http.Transport は、HTTP, HTTPS、および HTTP プロキシをサポートするための RoundTripper になります。

つまり、アプリケーションミドルウェアと同様に、第一引数へメインロジックを持つ RoundTripper を渡して、その後の引数に続けて RoundTripper を満たしたクライアントのミドルウェアを渡すことで実現可能となります。

Microservices が流行っている今の時代では、複数のサービスへリクエストを送るためにそれぞれに合わせたクライアントを用意する必要が出てきました。それぞれのクライアントにも共通のロジックを持たせる可能性があります。そこで今回のミドルウェアの作成に関する知見を活かせると良いですね!

「例外」がないからGo言語はイケてないとかって言ってるヤツが本当にイケてない件

 この記事は、Go3 Advent Calendar 2018 の8日目の記事です。
 7日目は @codehex さんによる「Go でアプリケーションとクライアントのミドルウェアを作成する方法知ってますか?」でした。

 本日はネタ全開でお送りいたします。

Disclaimer(免責事項)

 はじめに言い訳というか、これを書いた経緯というか。

 というツイートをいたしまして、言った手前自分でやるか、と思い立った次第です。
 なので、ネタとしてお楽しみください。

 なお、炎上した場合にも、それすらもネタとして楽しむ所存ですのでアシカラズ。

 それでは、いってみましょう。

Go言語がイケてない…だ…と……?

 Go言語はイケてない言語としてよくdisられているが、その中でも2大disられポイントがこれだ↓

  • Genericsがないm9(^Д^)プギャー
  • 「例外」がないm9( ゚,_ゝ゚)ブブッ

 前者をdisってるヤツらについてはすでに 別の人がdisっているので 、今回は後者、「例外」1 がないことをdisってるヤツらがいかにイケてないかdisることにする。

 なお、これ以降のコード例では、ライブラリのインポートや冗長なクラス宣言などは省略することにする。

事実:達人たちは「例外」を使わない

 おまいら、Googleの「Google C++ Style Guide」を読んだことはあるか?ここに日本語訳があるので、まずは読んでからdisりに来い。
 ここにあるように、GoogleではC++のコーディングで throw の使用を禁じている。それによる恩恵よりもデメリットのほうが上回ると考えているからだ。
 これはなにもGoogleに限った話ではない。LLVM Coding Standard も同様に例外機構の使用を禁じている

 V8やLLVMといった、世界で最も利用されているであろう言語処理系を開発している達人たちは、「例外」を使っていないのだ。彼らの開発した処理系が例外機構を実現しているのというのに、なんとも皮肉な話である。

いやいや、それはC++の話でしょ。C++はtry-catchの存在が考えられていなかった時代のコードとか、メモリ管理の煩雑さとかあるから、問題多いのはわかるよ。
でも、Goはそういうしがらみないじゃん。新しい言語を設計するなら、Javaみたいに「例外」をいれるのがモダンな言語としては常識なんだよ。そんなことも知らないの?

 オーケー、わかった。アンタの言っている「例外」ってのはJavaのtry-throw-catchのことなわけね?で、アンタはJavaの「例外」をどれだけ理解しているわけ?

そもそも「例外」ってなんなん?

 んじゃ一度、アンタらにとってのバイブル的な存在であるはずの『Java Language Specification』に立ち返って、「例外」とはなんだったのか考えてみようじゃまいか。

呼び出し元に異なる型の値を返せる手段としての例外

 「Chapter 11. Exceptions」では次のように書かれている:

Explicit use of throw statements provides an alternative to the old-fashioned style of handling error conditions by returning funny values, such as the integer value -1 where a negative value would not normally be expected.

※強調は筆者による

 奇妙な値(funny values)っていうのは、C言語でよく見られるこういうやつね↓

C
pid_t funny = fork();
if ( funny < 0 ) {
    // pidがマイナス値だとエラー
    fprintf(stderr, "Can't fork");
    exit(1)
} else if ( funny == 0 ) {
    // 子プロセスでの処理
} else {
    // 親プロセスでの処理
}

 これはたしかに、いろいろと問題だ。
 具体的になにがエラー原因だったのかは error 変数を参照しなければいけなかったりするし、なにより、正常時に返す値の型でエラーが表現できない場合に、どうやって呼び出し元にエラーを通知したらいいものか?

 たとえば、次のようにint配列の平均を求める関数があったとしよう:

C
int average(int* a, int n) {
    int s = 0;
    for ( size_t i=0; i < n; i++ ) s += a[i];
    return s / n;
}

 これを、次のように空配列に対して呼び出すと、

C
int main (){
    int a[] = {};
    int r = average(a, 0);
    printf("%d\n", r);
}

 コアダンプしてしまう:

[1]    939 floating point exception (core dumped)

 n == 0 の場合にはエラーを返すようにしたいが、呼び出し元にintしか返せないのに、どうやって呼び出し元に例外を知らせたらいいというのか?2 この関数は、正常な戻り値として 0 はもちろん負数だって返しうる。異常時のために使える"スキマ"はすでに int の中にはないのだ3

 こんなとき、Javaだったらこう書ける:

Java
static int average ( int[] a ) {
    if ( a.length == 0 ) {
        // 配列が空なら ArithmeticException を投げる
        throw new ArithmeticException("division by zero");
    }
    int s = 0;
    for ( int i=0; i < a.length; i++ ) s += a[i];
    return s / a.length;
}

 この関数は配列が空だったとき、

Java
public static void main(string[] args) {
    int a[] = new int{};
    try{
        System.out.println(average(a));
    } catch ( ArithmeticException e ) {
        System.err.println(e)
    }
}

 例外によってエラーを教えてくれる。しかも、その原因は ArithmeticException という人間にもわかりやすい型で教えてくれるのだ。

Every exception is represented by an instance of the class Throwable or one of its subclasses (§11.1). Such an object can be used to carry information from the point at which an exception occurs to the handler that catches it.

 メソッドの処理結果として(正常時の結果の型とは別に) Throwable を継承するオブジェクトを好きなように投げることができるので、このオブジェクトの中にエラーに関する情報などを入れれば、呼び出し元に例外についての詳細な情報も通知することができる。

 つまり、正常な場合と異常な場合とで異なる型の値を呼び出し元に返せるようにすることが、「例外」の役割の1つなわけだ。

大域脱出の手段としての例外

 また、「Chapter 11. Exceptions」では次のようにも書かれている:

During the process of throwing an exception, the Java Virtual Machine abruptly completes, one by one, any expressions, statements, method and constructor invocations, initializers, and field initialization expressions that have begun but not completed execution in the current thread. This process continues until a handler is found that indicates that it handles that particular exception by naming the class of the exception or a superclass of the class of the exception (§11.2). If no such handler is found, then the exception may be handled by one of a hierarchy of uncaught exception handlers (§11.3) - thus every effort is made to avoid letting an exception go unhandled.

※強調は筆者による

 例外が発生したその場で処理されなかったとしても、呼び出し元を遡っていって、処理してくれるハンドラ(=型が合致するcatch節)を探すことで、なんとかして例外が処理されるように努力を尽くすというようなことを言っている。

 つまり、こういうことだ:

Java
static float variance ( int[] a ) {
    int s = 0;
    int av = average(a);
    for ( int v : a ) {
        v -= av;
        s += v*v;
    }
    return s / a.length;
}

public static void main(string[] args) {
    int a[] = new int{};
    try{
        System.out.println(variance(a));
    } catch ( ArithmeticException e ) {
        System.err.println(e)
    }
}

 average を呼び出している variance では例外を処理していないが、それを呼び出している main で見事に例外が補足されて処理されている。
 ここで、average 自身の実行はもちろん、それを呼び出している variance も一気に飛び越えて、main に実行が戻ってきている。こうした入れ子になった関数呼び出しを一気に巻き戻すことを「大域脱出」と呼ぶ。

 古式ゆかしいエラーを示す値を返す方法では、経験上、戻り値が無視されることが多かったと上で書いてあった。そのため、たとえその場では無視されてしまったとしても、呼び出し元の誰かが処理してくれることを期待して、その誰かの元へと一気に脱出することが、「例外」のもうひとつの役割だと言えよう。

 実際のところ、JavaScriptのように変数の型がない動的型付け言語では、return文で任意の型の値が返せてしまう:

JavaScript
function average ( arr ) {
    if ( arr.length == 0 ) return new Error("division by zero");
    return arr.reduce((x, y) => x+y) / arr.length;
}

let r = average([]);
if ( r instanceof Error ) {
    console.log(`Something wrong!: ${r}`);
} else {
    console.log(`OK: ${r}`);
}

 そのため、動的型付け言語においては、事実上この大域脱出のみが例外機構の役割と言ってもいいだろう。
 つまり、return文で普通に返ってきたのではなく、throw文で大域脱出してきた場合には「なんか普通と違うことがあったな」と捉えるという不文律の上に、JavaScriptの「例外」は例外機構として成立している。

function average ( arr ) {
    if ( arr.length == 0 ) throw new Error("division by zero");
    return arr.reduce((x, y) => x+y) / arr.length;
}

try {
    let r = average([]);
    console.log(`OK: ${r}`);
} catch ( e ) {  // if文よりもこう書いたほうが、「なんか普段と違う」感がある
    console.log(`Something wrong!: ${e}`);
}

 もちろん、これ自体は良い約束事だ。4

 なるほど、よく考えられてるわー。

ほれみろ、やっぱり例外は叡智の結晶であってイケてる機能なのだ。そして、それがない言語はイケてないんだ!

 ちょっ!待てって。そんな結論あせんなし。

分けて議論しようじゃないか

 いわゆる「例外」が議論されるとき、上の2つが無意識にごっちゃになって議論されていることが話が混乱する原因じゃねーのかな?でも、この2つが不可分であると誰が決めたわけ?
 別々に提供されていても、例外機構としては問題ないんじゃないか?むしろ、別々になっているほうが良いこともあるんじゃね?

は?別々?そんなの例外機構じゃねーよ

 だから決めつけんなし!まずは考えてみるべ。

複数の値を返せる多値返却

 Go言語の場合、throw に頼らずとも、異なる型の値を返すことができる:

Go
func average ( a []int ) (int, error) {  // 値を2つ返す関数
    if len(a) == 0 {
        return 0, errors.New("division by zero")
    }
    s := 0
    for _, i := range a {
        s += i
    }
    return s / len(a), nil
}

func main () {
    a := []int{}
    r, err := average(a)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("OK: %d\n", r)
}

 このように、正常時の結果とエラー時の原因を表す型の値(普通は error インターフェースを実装した値)の2つを関数から返すようにして、エラーの値をチェックするように書くのがGo言語の例外処理の流儀である。
 なので、try-throw-catchなんてものがなくても、例外処理はちゃんとできるのだ。

複数の値が返せるとか、キモっw
そんな変な言語なんて誰が使ってんだよ?ww

 は?まじで言っちゃってんの?
 多値を返せる言語なんていくらでもあんべ。

 90年台にはCGIのデファクトスタンダードだったPerlだって↓

Perl5
sub average {
    my $a = shift;
    return (0, "division by zero") if @$a == 0;
    my $s = 0;
    $s += $_ foreach @$a;
    return $s / @$a, undef;
}

my ($r, $e) = average([]);
if ( defined $e ) {
    die $e;
}
print "OK: $r\n";

 機械学習の流行で今をときめくPythonだって5

Python
def average(a):
    if len(a) == 0:
        return 0, Exception("division by zero")
    s = 0
    for i in a:
        s += i
    return s / len(a), None

av, err = average([])
if err is not None:
    print err
    exit(1)
print av

 超美しい型システムで有名なHaskellさんだって↓6

Haskell
average :: [Int] -> (Int, String)
average as = if (length as) == 0
                 then (0, "division by zero")
                 else ((foldl (+) 0 as) `div` (length as), "")

main = do (r, e) <- return (average [])
          if e /= "" then putStrLn e
                     else putStrLn $ "OK: " ++ show r

 みんな多値返却できるわ。
 むしろ、引数が複数とれるのに返り値は複数かえせないことのほうがキモいんじゃ!

 多値返却がないから、「例外」とかいう大仰そうなもの持ち出してきて自分の無力さを偉そうに自慢するなんて、まじイケてねーわーw むしろ痛えわーww

で、でも、それ大域脱出のほうができてねーじゃん!

 お?じゃあ、次いっちゃう?

大域脱出がないと誰が言った?

 たしかにGo言語にはthrowもcatchもないけど、誰も大域脱出ができないなんて言ってない。
 panicdeferrecoverという機能があるんだぜ:

Go
func average ( a []int ) int {
    if len(a) == 0 {
        panic(errors.New("division by zero"))  // ここから↓のdeferまで一気に脱出する
    }
    s := 0
    for _, i := range a {
        s += i
    }
    return s / len(a)
}

func variance ( a []int ) float64 {
    s := 0.0
    av := average(a)
    for _, i := range a {
        t := (float64)(i - av)
        s += t * t
    }
    return s / (float64)(len(a))
}

func main () {
    defer func(){
        if e := recover(); e != nil {
            log.Fatal("Caught error: ", e)
        }
    }()
    a := []int{}
    r := variance(a)
    fmt.Printf("OK: %f\n", r)
}

 このように、panic を呼ぶとその呼び出し元の defer があるところにまで遡っていって、defer が見つかったらそれが実行される7。そして、defer の中では recover を使って panic が投げた値を捕捉(catch)できるというわけ。
 ね?ちゃんと大域脱出できてんしょ?
 どこの panic でどんな値が投げられてくるのかは型チェックされないから、実質的には上で書いたJavaScriptの throw と同じことが実現できてるっつーわけ。

defer とか recover とか、予約語のセンスが意味わかんねーんだけど・・・

 それ言ったら、Javaやってないヤツからしたら、throw とか catch とか意味わかんねーんですけど?
 日本語しかしゃべれねーヤツが英語わかんねーとかブーたれてんのと一緒だべ?まじイケてねーこと言ってねーで、他言語も勉強しろや。
 それにな、慣れるとこっちの書き方のほうがいいなって思える点もいろいろあんだよ!ブツクサいってねーで、慣れろ!!

そんなにtry-catch風に書きたいなら

 こういうこともできる↓

Go
package main

import (
    . "github.com/mattn/go-try/try"
    "errors"
    "fmt"
)

func average (a []int) int {
    if len(a) == 0 {
        panic(errors.New("division by zero"))  // throwの代わりにpanicで例外を投げる
    }
    s := 0
    for _, i := range a {
        s += i
    }
    return s / len(a)
}

func main() {
    Try(func() {
        av := average([]int{})  // 空配列を渡しているので、
        fmt.Println(av)         // この行は実行されない
    }).Catch(func(e error) {
        fmt.Println(e)          // averageからここに飛んでくる
    })
}

 Go言語なら、その気になればtry-catch風の書き方をライブラリとして実装できるというわけだ。
 ただし、これは推奨された書き方ではない8

なぜ Go は「大域脱出」を例外処理として採用しなかったのか?

そんなんあるなら、じゃあなんで標準ライブラリで if f, err := os.Open(...); err != nil { ... } とかやってんだよ?全部 panic 使ってJavaっぽくすりゃ良かったじゃん?

 あー、それな。
 それについては、Dave Cheneyさんが歴史をおって解説してくれている
 詳しくは全文を読んでほしいけど、一部を引用しておこう:

Java exceptions ceased to be exceptional at all, they became commonplace. They are used from everything from the benign to the catastrophic, differentiating between the severity of exceptions falls to the caller of the function.

 いろんなことに例外を使った結果として、「Javaの例外はまったく例外的なものではなくなってしまった」と。そして、どの「例外」がどれくらい深刻なものなのか(つまり、正常な状態に復帰させるべき例外はどれで、すぐさまシステムを停止させるべき例外はどれか)を見分けるのは(例外を起こした側ではなくて)関数の呼び出し側に丸投げされることになってしまった。

 極めつけは、

because every exception Java extends java.Exception any piece of code can catch it, even if it makes little sense to do so, leading to patterns like

catch (e Exception) { // ignore } 9

 こういうパターンがまかり通るようになってしまった。

 このパターンがどんなに理に適っていないとしても、また、わかったつもりで使っているのだとしても、利用しているメソッドがどんなExceptionを投げてくるのか完全にはわからない以上、知らないうちに重要な例外を握りつぶしてしまうということが起こる。
 自分でも気づかないうちに例外を握りつぶしてしまうことは、システムを予測不可能な状態にしてしまうし、もちろんデバッグも困難にさせる。

 Java Language Specification の「Chapter 11. Exceptions」には次のようにも書かれている:

Experience shows that too often such funny values are ignored or not checked for by callers, leading to programs that are not robust, exhibit undesirable behavior, or both.

 例外が無視されて誰にも処理されずに終わってしまうことのないよう、なるべく誰かに catch されるようにした結果、ある意味で catch しすぎてしまったとも言える。その結果、robustなプログラムを書くための弊害となってしまったのであれば、なんとも皮肉な話である。

そ、そんな Exception を catch するだけして無視しちゃうような乱暴なコード、誰も書かないし・・・

 そうな、アンタはそうなのかもな。
 でも、もしウソだと思うんなら、GitHubを見てから 語り合おうや。

 一方でGoでは、例外は呼び出し元が責任をもって処理をするべきという考えを持っている。言い換えれば、システムが "例外的な状態" におちいったときには呼び出し元が "正常な状態" へと戻すことが期待されている。そして、"正常な状態" へと戻すことが期待されないような状態におちいったときには panic を使うべき という約束の上に、Goの例外処理は成り立っている。
 本来あってはならないような状態に陥ってしまった場合には、その場を取り繕うようなことはせず、さっさとプログラムを停止して、コードを修正すべきだという考えに立脚しているのだ。
 プログラムを早く修正できるようにするため、なるべく早期にプログラムを停止し、異常な状態におちいった箇所のなるべく近くで状態を調べ上げレポートするべきという信条にもとづいているのである。(これは Fail-fast の考え方である)

 それから、並行処理を書くにはthrow-try-catchは適さないって話 もある。
 もともとGoの設計ゴールのひとつは、 並行処理を安全に効率よく書けるプログラミング言語 となることだった。そのため、並行処理と相性の悪い try-catch 方式は、Goの推奨されたやり方にはなれなかったのだ。まぁ、詳しくは読んどいてくれや。

それ、本当に型安全だと思ってんの?

ちょぉっと待っっったーーー!!

 む、新手か!?

さっきから黙って聞いていれば、メソッドがどんな Exception を投げてくるかわからないだと?どうやらキサマは検査例外のことも知らんようだな?
Java言語仕様には 検査例外(Checked exceptions) という極めて洗練された静的型チェックの仕組みがあるということを!!!

たとえば、一見正しそうなこのJavaプログラム、

Java
import java.io.*;

class Main {
    public static void main(String[] args) {
        new File("tempfile").createNewFile();
    }
}

これをコンパイルしようとすると、ほれ、このとおりエラーとなる:

5: error: unreported exception IOException; must be caught or declared to be thrown
        new File("tempfile").createNewFile();
                                          ^
1 error

エラーメッセージが示しているように、createNewFileIOException を投げるメソッドであり、呼び出し元がそれを catch していないことをコンパイラが検知してくれるのだ。
なぜJavaではこのようなことが可能なのかというと、Javaでは各メソッドが自らが投げる可能性のある例外を型情報として持っているのだ。たとえば、createNewFile のシグネチャはこのとおり

public boolean createNewFile()
                      throws IOException

自ら IOException を投げると宣言している。
こういった例外を投げるメソッドを使っているプログラムのコンパイルを通すには、このように例外をちゃんと catch するか、

Java
public static void main(String[ ] args) {
    try{
        new File("tempfile").createNewFile();
    } catch (IOException e) {
        System.err.println(e);
    }
}

もしくは、自分の呼び出し元が正しく catch して処理できるように、自らもまた IOException を投げることを宣言しなくてはならない:

Java
public static void main(String[ ] args) throws IOException {
    new File("tempfile").createNewFile();
}

従って、どんな例外が投げられるのかわかっているため、"知らずに重要な例外を握りつぶす" などという妄挙は起こり得ぬのだ!
見たか、Java言語の型システムの素晴らしさをぉぉぉ!!

(ちっ、めんどくさいヤツ来たな…)

 もちろん、俺だって検査例外のことを知らないわけじゃない。
 Dave Cheneyさんも書いているように、検査例外はC++の「例外」の問題点をうまく克服しているいいアイデアだって、最初はみんな思ってた。新しいものが発明されるときってのは、いつだってそうさ。
 でも、歴史がいつだってそう証明してくれているように、現実はそんなにうまくはいかなかった。

 例として、入力としてユーザー名とパスワードを受け取って認証結果のtrue/falseを返すメソッドについて考えてみよう。
 パスワードを照合する実装としては、/etc/passwd のようなファイルを使う場合や、LDAPのようなデータベースに問い合わせる場合などいろいろ考えられるから、実装が切り替えられるように抽象化しておきたい。
 これは Authenticator という次のようなインターフェースで表せるだろう:

Java
interface Authenticator {
    boolean authenticate(String name, String password);
}

 そして、これを実装する FileAuthenticator クラスはこう書けそうだ:

Java
class FileAuthenticator implements Authenticator {
    @Override
    public boolean authenticate(String name, String password) throws FileNotFoundException {
        File f = new File("/etc/passwd");
        FileReader r = new FileReader(f);
        ...()
    }
}

 でもこれ、コンパイル通らないんだぜ:

error: authenticate(String,String) in FileAuthenticator cannot implement authenticate(String,String) in Authenticator
        public boolean authenticate(String name, String password) throws FileNotFoundException {
                       ^
  overridden method does not throw FileNotFoundException
1 error

 なんで怒られてるかっつーと、「オーバーライド元のメソッド(インターフェースの宣言)が FileNotFoundException を投げるって宣言してないのに勝手に投げるな」って言われてるわけ。

それはそうだろう。呼び出し元は Authenticator を使うとしか思っていないのだから、Authenticator が投げないと言っている例外を勝手に投げられては、検査例外のコンパイル時チェックが働かなくなってしまうではないか。仕様にそった正しい動作である。

 じゃあ、呼び出し元にファイルが開けなかったってどうやって知らせるわけ?

例外を投げる必要があるのであれば、インターフェースでそれを明示するべきである。つまり、こう書くべきである:

Java
interface Authenticator {
    boolean authenticate(String name, String password) throws FileNotFoundException;
}

 だよね。そうなるよね。
 じゃあ、これに加えてLDAPを使った実装を作ろうとすると LdapException が投げられる可能性もあるんだけど、どうすんの?

こ、こう書くのである…

Java
interface Authenticator {
    boolean authenticate(String name, String password) throws FileNotFoundException, LdapException;
}

 あとさ、MySQLに問い合わせる実装も追加するかもしんないんだけど・・・・それ、毎回インターフェース宣言書き換えなくちゃいけないの?
 っつーか、実装を抽象化してるはずのインターフェースが、FileNotFoundException だったり LdapException だったりって実装固有の情報を持っちゃってるって、抽象化としてどうなの?

ぐぬぬ・・・・・・

 というように、検査例外は実装の詳細を不用意にさらけ出しているという批判は昔からある問題だ。

 実装元のインターフェースが書き換え可能な場合はまだマシ10で、実装しなくちゃいけないインターフェースがサードパーティのフレームワークのものだったりすると、書き換えて対応することすらできない。

 こういうことはよくあるわけで、実際にはどうすりゃいいの?っていうと、こういうワークアラウンドがよく知られている:

Java
class FileAuthenticator implements Authenticator {
    @Override
    public boolean authenticate(String name, String password) {
        try {
            File f = new File("/etc/passwd");
            FileReader r = new FileReader(f);
            ...()
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

 例外を RuntimeException でラップして投げ直してしまえばいい。

 この RuntimeException は非検査例外って呼ばれてるものの一種で、コイツとコイツのサブクラスはコンパイル時のチェックを受けない11。なので、authenticate メソッドのシグネチャでも宣言しなくていい。
 もし RuntimeException で例外を隠してしまわないようにしたいというのなら、プログラムを大きく書き換えなくてはいけない。多くの人はそんなことしたくないから、結果として、こういうワークアラウンドが蔓延することになるわけだ。

 というわけで、さっきの「メソッドがどんな例外を投げるのかはわかっている」というのはウソで、メソッドシグネチャを見ただけでは、どんな RuntimeException が飛んでくるのかはわからない。
 しかもタチの悪いことに、みんな大好きヌルポ(NullPointerException) がこの RuntimeException のサブクラスだったりする12

 なのでさっきの例を使って、

Java
// ボクは FileNotFoundException と LdapException を投げることがあるから気をつけてね!
interface Authenticator {
    boolean authenticate(String name, String password) throws FileNotFoundException, LdapException;
}

public class Main {
    // 実はバグってて、ヌルポを投げることがある実装↓
    private static Authenticator auth = new MyAuthenticator();

    public static void main(String[] args) {
        try{
            boolean ok = auth.authenticate(args[0)], args[1]);
            if ( ok ) {
                System.out.println("OK");
                System.exit(0);
            }
        } catch (Exception e) {
            // FileNotFoundException か LdapException が投げられるかもしれない
            // けど、どちらの場合も失敗として無視すればいいや
        }
        System.out.println("NG");
        System.exit(1);
    }
}

 とかやって、NullPointerException も握りつぶしてしまって、なかなかバグに気づけないということが起こるわけだ。

 検査例外が完全に間違ったアイデアだったとまでは言わない13。でも、検査例外はひとつの問題を解決すると同時に、別の問題を作り出してしまったんだ。つまり、トレードオフがある。しかし、そのトレードオフに表面上は気づきにくいがゆえに、罪が深い。
 Javaよりもあとに設計された言語である C# や、Javaの後継とすら言われることのある Scala が検査例外を採用しなかった14のには、それなりの理由があるってわけだ。

 Goも同じだ。Java後の歴史をよく反省した上で、抜本的な解決策として大域脱出を「例外」として使わない道を選んだんだ15
 Goに try-throw-catch がないことで、まるで Java よりもすごい退化した言語のようにdisるヤツらがいるが、そういうおまいらこそが、Java後の世界から何も学ばず進化できてないんだってこと、そろそろ自覚しねーと、まじでやべぇぞ。

ある関数言語話者の感想

ずーっと横で聞いてたんだけどさ、

 うおっ!?また新手かよ!

キミたち、大域脱出くらいでよくもまぁ、そんなに熱くなれるよね。
大域脱出なんて、言語で直接サポートしなくても、ライブラリとして提供すればよくない?こんなかんじで↓

Scheme
(require 'macro) ; Required to use this with SCM. Other Scheme implementations
                 ; may not require this, or it may even be an error.
(define *cont-stack* '())

(define (push-catch cont)
  (set! *cont-stack* (cons cont *cont-stack*)))

(define (pop-catch)
  (let ((retval (car *cont-stack*)))
    (set! *cont-stack* (cdr *cont-stack*))
    retval))

(define (throw exn)
  (if (null? *cont-stack*)
      (error "Can't use throw outside of a try form")
      ((car *cont-stack*) exn)))

(define-syntax try
  (syntax-rules (catch)
    ((try (body ...)
      catch exception (catch-body ...))
     (call-with-current-continuation
      (lambda (escape)
    (dynamic-wind
        (lambda ()
          (let ((exception
             (call-with-current-continuation
              (lambda (cc)
            (push-catch cc)
            #f))))
        (if exception
            (begin catch-body ... (escape)))))
        (lambda ()
          (begin body ...))
        (lambda () (pop-catch))))))))
> (try ((display "one\n")
        (throw "some-error\n")
        (display "two\n"))
   catch err ((display err)))
one
some-error

(※SCMで実行してください)

あれ?もしかしてキミたちの言語、継続が第一級の計算対象になってないの?
それやばくない?例外がどうとか言ってる前に、そっちのほうがまじでイケてなくない?

 ・・・・・・・。
 おあとがよろしいようで。

References


  1. 文中で「例外」とあえてカッコ書きにしているのは、暗にtry-throw-catchとその類型のことを指しています。本来的には例外とは処理の結果が通常どおりにはいかなかったことを指すので、それをreturnで表そうがthrowで表そうが構わないはずです。しかし、try-throw-catchがないことを指して「例外がない」と揶揄されることが多いので、「例外」とカッコ書きで使うことにしました。 

  2. 「そんなときのためにC言語には longjump という機能があってじゃな…」というC言語賢者のあなたは、下の大域脱出の話まで読み飛ばしてください。 

  3. intにはスキマがなくても、floatだったら NAN (not a number) というスキマがある。割り算は小数が発生しうるのに、ここでの例をfloatではなくあえてintにしているのはこのため。まあ、説明のためであって、実用プログラムではないのでご理解ください。 

  4. 裏を返せば、JavaScriptのtry-throw-catchのようなものを例外機構と認めている人にとっては「例外機構=大域脱出」でしかないということを示しています。勝手な印象ですが、「例外」がないことをdisっている人の9割は大域脱出の部分しか意識していないように思います。 

  5. これは多値というより、タプルという1つの値なのでは?という意見もありましょうが、細かいことは置いといてください。 

  6. これも多値というよりタプルなんですが、細かいことは(ry また、Haskellならこういう場合はMaybe なり Either なりを使うべきってご意見もあるでしょうが、モナドの説明をするにはこの余白はあまりにも狭す(ry 

  7. 正確にいうと、defer が実行されるのは panic が呼ばれたときだけではありません。defer が書かれた関数から抜けるときに必ず実行されます。try-finally を知っている人にとっては、finally節と同じだと思えばいいでしょう。Goでは finally のなかで catch に相当する処理を行うわけですね。 

  8. 念のため、mattnさんのライブラリが良くないというわけではなく、Go言語の流儀ではないということです。 

  9. このコード片は引用元の記事からそのまま転載しています。懸命なJavarista16諸氏はお気づきでしょうが、これは正しいJavaコードではありません。正しくはこう書くべきでしょう: catch (Exception e) { /* ignore */ } 

  10. これをマシと言ってしまっていいのかは、意見がわかれるところでしょうね。 

  11. 補足すると、Error も非検査例外の一種であり、Javaの型階層の中ではこの Error あるいは RuntimeException とそれらのサブクラスのみが非検査例外であり、それ以外のすべての Throwable が検査例外と決められています。 

  12. あるいは NullPointerExceptionArrayIndexOutOfBoundsException といった如何にもコーディングのミスで起きてしまいそうな例外が RuntimeException ではなく Error のサブクラスだったとしたら、Java の例外処理はもう少し平和な世界になっていたのかもしれない・・・・・・と起こり得なかった世界線について思いを馳せることもあります。 

  13. 実際、近年設計された言語であるSwiftはJavaの検査例外に似た機能を持っています。ただし、投げられる値の型は書かないという大きな違いがあります。これは「失敗する可能性のある計算とそれ以外 だけ は区別できるようにしよう」という試みで、実質的にはHaskellのMaybeと同じコンセプトと考えられます。投げられる値の型を1つだけ指定できるようにしようという議論もされているらしいですが、その場合にもやはりHaskellのEitherに相当するものと言えます。Javaの失敗をよく踏まえて設計されていると感じます。 

  14. なお、JavaからScalaのメソッドを呼び出したときの互換性を確保するために、@throws アノテーションを使って例外を投げることをJavaの呼び出し元に知らせることはできる。でも、アノテーションをつけた場合でも、やっぱりScalaの世界ではコンパイル時チェックされません。 

  15. この選択そのものに対する意義はあってもいいと思っています。が、意義をとなえるなら、それに代わるベターな対案を提示するべきでしょう。その意味では、Haskell や Scala の Either を使う例外処理のようなコンポーザブルな手法と比較するのは有意義でしょう。 

  16. http://www.nilab.info/z3/20120708_04.html 

Goの開発環境でDockerを利用する

Go3 Advent Calendar 2018の9日目を担当するtheoden9014です。よろしくお願いします。

Makefileについて書こうと思ったのですが、内容が同じような記事が先に出てしまったようなので、少し趣向を変えたタイトルにしました。

この記事では、MakefileDockerfileを上手く利用することによって、Docker内でのビルドやデバッグを簡単にする方法についてご紹介します。
この手法であれば、開発者は手元にはDockerをインストールするだけでGoを使った開発を行えるようになります。

今回は以下のリポジトリをベースとしてご紹介していきます。
https://github.com/theoden9014/go-startkit/tree/qiita

背景

Goで開発する際には$GOPATHの知識は必要不可欠です。
vgoの普及によって$GOPATHの重要性は下がってくるでしょうが、現段階では全く考慮しないわけには行きません。
初心者でも簡単にHello Worldができるような環境を作りたいな、と思い今回の記事を書きました。

Dockerfile

以下のDockerfileはdockerのマルチステージビルドを用い、以下のステージに分けています

  • development
    • 開発を行うための周辺環境が整っているステージ
  • builder
    • ビルドを行うための周辺環境が整っているステージ

ステージを指定しない場合は、builderステージでビルド後、成果物をalpineへコピーし、バイナリを実行するだけのコンテナイメージをビルドすることができます。

#
# Use multi stage build so require docker version higher 17.05.
#
FROM golang:1.11 AS development
ARG name=
ARG repository=
RUN apt-get update -y
RUN apt-get install -y doxygen doxygen-gui graphviz
WORKDIR /go/src/${repository}
COPY Makefile ./
RUN make setup
CMD ["/bin/true"]

FROM development AS builder
COPY Gopkg.toml Gopkg.lock ./
RUN make vendor
COPY . .
RUN make build
CMD ["/bin/true"]

FROM alpine
ARG name=
ARG repository=
ENV PORT 8080
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/${repository}/${name} ./app
ENTRYPOINT ["./app"]

Makefile

https://github.com/theoden9014/go-makefile/blob/qiita/Makefile
上記のMakefileのDockerに関する部分について軽く説明していきます。

REPOSITORY              ?= github.com/theoden9014/go-startkit
NAME                    ?= go-startkit

...

#
# Docker
#
DOCKER                          ?= docker
DOCKER_IMAGE_NAME               := $(NAME)
DEVELOP_DOCKER_IMAGE_NAME       := $(NAME)-dev
BUILD_DOCKER_IMAGE_NAME         := $(NAME)-build
DOCKERFILE                      := Dockerfile
DEVELOP_STAGE                   := development
BUILD_STAGE                     := builder
DEFAULT_DOCKER_BUILD_OPTS       := -f $(DOCKERFILE) --build-arg name=$(NAME) --build-arg repository=$(REPOSITORY)
DOCKER_BUILD_DEVELOP_STAGE_OPTS := $(DEFAULT_DOCKER_BUILD_OPTS) -t $(DEVELOP_DOCKER_IMAGE_NAME) --target $(DEVELOP_STAGE)
DOCKER_BUILD_BUILD_STAGE_OPTS   := $(DEFAULT_DOCKER_BUILD_OPTS) -t $(BUILD_DOCKER_IMAGE_NAME) --target $(BUILD_STAGE)
DOCKER_BUILD_OPTS               := $(DEFAULT_DOCKER_BUILD_OPTS) -t $(DOCKER_IMAGE_NAME)

...

# Docker targets from here
docker: ## Build server image
    docker build $(DOCKER_BUILD_OPTS) .
login: build-develop-stage ## Login to development environment with container
    docker run --rm -it \
    -v $(PWD):/go/src/$(REPOSITORY) \
    -p 8080:8080 \
    $(DEVELOP_DOCKER_IMAGE_NAME) \
    /bin/bash
docker-check: build-develop-stage ## Run 'make check' inside container
    docker run --rm -it \
    $(DEVELOP_DOCKER_IMAGE_NAME) \
    make check
docker-coverview: build-develop-stage ## Run 'make coverview' inside container
    docker run --rm -it \
    -v $(PWD):/go/src/$(REPOSITORY) \
    $(DEVELOP_DOCKER_IMAGE_NAME) \
    make coverview
docker-graph: build-develop-stage ## Run 'make graph' inside container
    docker run --rm -it \
    -v $(PWD):/go/src/$(REPOSITORY) \
    $(DEVELOP_DOCKER_IMAGE_NAME) \
    make graph
build-develop-stage:
    docker build $(DOCKER_BUILD_DEVELOP_STAGE_OPTS) .
build-build-stage:
    docker build $(DOCKER_BUILD_BUILD_STAGE_OPTS) .

.PHONY: docker login docker-check docker-coverview docker-graph build-develop-stage build-build-stage

上記のMakefileを利用することで以下のようなことが行えるようになります。

  • make login
    • プロジェクトルートディレクトリをコンテナ内の$GOPATH配下へマウントし、コーディングはローカルのIDEで、ビルドやデバッグはコンテナ内で行えます
  • make docker-*
    • コンテナ内でテストやパッケージ依存関係のグラフ作成等を行えるようにしています

最後に

以上、簡単ではありましたがGo言語をDockerで開発する方法の紹介でした。
ここまでご覧いただきありがとうございました。

明日は@pankonaさんです!

ひとりかんばん的な ToDo 管理アプリ「hashira」を Go で作っているぞ

この記事は、Go3 Advent Calendar 2018 の 10 日目の記事です。
どうもいつも元気な @pankona です。

長い話を短くすると

  • 現在製作中な ToDo 管理アプリの紹介です。名前は「hashira」。Go で作ってます。

    • 見た目はこんな感じです。
    • 柱が 4 本立ってるっぽく見える気がするので hashira という名前にしました。 Screenshot from 2018-12-10 00-16-53.png
    • ターミナル上で動作する (TUI) ひとりかんばんライクなやつです。いまのところスタンドアロン (つまりインターネットなしで) 動作します。
    • リポジトリは https://github.com/pankona/hashira
      • (2018/12/25 追記) 最新リリースは v1.7.0 です。
      • バイナリリリースは Linux と Mac 向けのみ同梱。Windows 版は動作未確認なので含んでいません、ごめんなさい!
    • もし興味があったら試してみてね!動かし方は README.md を参照してください!
    • 2018/12 現在、鋭意製作中のため、粗が目立つかもしれませんがどうかひとつ生暖かい目で見守っていただければと…。

以下、hashira の紹介

hashira とは何か

ターミナル上で動作する (いわゆる TUI を持つ) ToDo 管理アプリです。
ひとりかんばんライクな UI (Backlog、ToDo、Doing、Done がある) で構成しています。

工夫している点

ToDo アプリ作った!ってそれだけだと単なる習作のようにも聞こえてしまうかもしれないのですが、
hashira に関してはいくらか工夫しているところがあります。

新しく追加した ToDo は一番上に追加される

個人的にこれが一番やりたかったみたいなところあるんですが、世の中の ToDo 管理アプリの多くは、"新しく追加した ToDo をリストの末尾に追加する" 動作をするような気がします。

ですが、それだとリストが長くなってくるに従って "新しく追加した ToDo がスクロールしないと見えなくなってしまう" 状況になりがちです。いや、私のようにそんなリストが長くなるような使い方をしている時点でちょっとあれなのかもしれないですけど、私のいままでの個人的な ToDo 管理アプリの使い方として、「靴を買う」とか「シャンプー買う」とか「子の上履きを洗う」などの細かーい ToDo まで入れていきたいという気持ちがあり、この手のまで入れていこうとすると、どうしても ToDo のリストが長くなりがちになっちゃうというのは何となく想像いただけるのではないかと思います。

ちなみに私がいままで使っていた ToDo 管理アプリのてっぺんには「英語を勉強する」という一生終わらなそうな ToDo が鎮座ましましており、消すに消せず、「いや英語勉強するし」と毎日ひとりごちてはそのまま放置してしまい、結果 ToDo リストのてっぺんという特等席を占拠し続けて数年経過する、みたいなことになってしまうこともあるわけです。

私の例はちょっと極端な例かもしれませんが、新しく追加した ToDo を一番上に追加していくことで、この「英語を勉強する」のような生存期間が長い腐った ToDo が勝手に押し出されて見えなくなっていくと共に、最近追加した活きの良い ToDo がよく見えるところに置かれるので嬉しい、のような効能があるように感じています。

TUI

我々ソフトウェアエンジニアの多くは、おそらくターミナルを操作することに多くの時間を割いていることだろうと想像します。なので、ToDo 管理もターミナルで完結できるとコンテキストスイッチの面で効率いいと思うわけです。ターミナルから離れたくないぞ。

想定する使い方

こんな使い方を想定しています。

  • 思いついた ToDo はすべて Backlog にいれる。
  • 今日や明日、明後日くらいまでにやるべき何となく優先度が高いものを ToDo に移す。終わったら順次 Done に送る。
  • 作業していて発生した割り込み作業や、ふと思いついた何かとかも適当に Backlog か ToDo に放り込んでいく。
    • 優先度の高い割り込み作業なんかも、新規に追加した ToDo は一番上に出てくるという動作のおかげで、ToDo として追加したけど見えなくなっちゃって忘れてしまった、なんてこともなりにくいはずだぞ!

これから先の hashira

  • いまは TUI のみですが、今後はとりあえず Android アプリ (ウィジェット) を作る予定。
    • 細かい ToDo を日常的に登録したいならばターミナルだけだとキビシイよね、PC 開けてないときもあるし…。
    • クラウド側での認証、ToDo のデバイス間での同期機能をぼちぼち実装中。
  • Done に送った ToDo は消えないので、ひたすら長くなってしまう。Done に積まれた ToDo は一定期間がすぎたら自動的に見えなくするようにする予定。
  • ToDo が多くなりすぎたときに所望の ToDo を探せるよう、フィルターして表示する機能も入れる予定。

その後ウェブアプリ版も、等と思っていて青写真はまあまあ壮大なのですが、いかんせんひとりで業務の隙間時間に作っているようなやつなので、このままだと出来上がるまで 100 年かかりそうな開発ペースです…。

制限

正確には使っている TUI フレームワークライブラリである gocui の制限ですが、日本語を始めとしたマルチバイトキャラクターを正しく入力/表示できません。キビシイ。直し方は分かっているので、近いうちに直します…!
(2018/12/25 追記)
hashira v1.7.0 にて、日本語入力をサポートしました!

終わりに

完全に自分のために作っている ToDo 管理アプリの紹介でしたが、いかがでしたでしょうか。
まだまだ作りかけの状態ですが、おっ、なんか面白いな、みたく思っていただければ本望です。
もしも興味をもっていただけたならば、ちょいと使ってみていただけると嬉しいです。フィードバックいただけるならなお嬉しい...!

明日は @kumakuma さんの登板予定です!お見逃しなく!

goroutineとスレッドの違いって?

この記事は、Go3 Advent Calendar 2018 の11日目の記事です。

はじめに

Goにはgoroutineという並行処理を簡単に実行できる機能があります。
goroutineは「goroutineとは軽量スレッドである」と説明されていますが、結局goroutineとスレッドって何が違うのかということを調べてみました。

まず最初に言うとgoroutineの実体はスレッドです。
ですが大きく3つの違いがあります。順番に見ていきましょう。

メモリ消費量

スレッドのデフォルトスタックサイズは Windows だと 1 MB、Linux だと 2 MB で
goroutineは数キロバイトで済みます。
必要に応じてヒープ領域を割り当てたり開放したりします。一方でスレッドはスレッド間のメモリが干渉し合わないように「スタックガードページ」と呼ばれる 1Mbの領域の確保から始めます。
数キロバイトで済む goroutine に比べると、10 KB だとしても100〜200倍の違いがあります。

生成と破棄に要する時間

スレッドは生成と破棄のたびに OS に要求を投げて、それが完了して返ってくるのを待つため時間がかかります。
一方で goroutine では、生成と破棄に関する操作を非常に低コストで行うことができます。

スイッチングに要する時間

スレッドがブロックされると、別のスレッドがスケジューリングされます。 プリエンプティブ方式でスレッドがスケジューリングされ、スレッドの実行がスイッチされる際にスケジューラーは全てのレジスタ、つまり 16種類の汎用レジスタ、PC(プログラムカウンタ)、SP(スタックポインタ)、16種類の XMM レジスタ、 FP co-processor の状態、16種類の AVX レジスタ、そしてモデル固有のレジスタを別の場所に保存したり、保存したそれらをレジスタに戻す処理が必要になります。これはスレッド間で迅速にスイッチングしたい場合に、無視できません。
goroutine は協調してスケジューリングされ、スイッチングが発生したときも、たった 3つのレジスタ(PC、SP、DX)しか保存したり、レジスタに戻したりしません

まとめ

Goは並行処理をするにあたってgoroutineを使うことによってメモリ消費量を抑えてました。
メモリ使用量が多くなり過ぎると、スワップが発生し性能低下を招くため、小さなメモリ使用量で済むというのは大きな利点だと思います。
ここで述べた以外にもgoroutineはスレッドよりも起動時間が早かったり、メモリ消費量がすくないためスレッドに比べてもより多く
の並行処理を実行することができるとも言われています。(他にも利点は色々とある)
並行処理が簡単に実行できて、スレッドよりも数多くの並行処理を実行できるgoroutineという機能があるGoは最高ですね。
(気付いた点等ございましたら、お知らせください)

Go でテキストエディタを開発しよう!!

この記事は Go3 Advent Calendar12日目の記事です。

Target

この記事のターゲットは以下です。

  • テキストエディタを開発することに必要なことを知る
  • Go でテキストエディタを開発することに必要なことを知る
  • 筆者がテキストエディタ開発で体験したこと(得られたこと)を知る

Background

私は、テキストエディタが好きです。
テキストエディタが好きだから、テキストエディタを作ります。
みなさんも、テキストエディタが大好きですよね??

まだ、テキストエディタを作ったことのない読者様、
テキストエディタを作って、テキストエディットについて、
もっと理解をしてみませんか??

ほぼ全ての人間の作るプログラム(ソースコード)はテキストエディタから生まれます。
テキストエディタは、私たちプログラマの創造を具現化する Interface です。
さあ、一緒にテキストエディットの世界へ行きましょう!!

なお、筆者の開発しているテキストエディタは以下の GitHub リポジトリにあります。
※ この記事では古い version のソースコードを紹介していますので、最新のソースコードは以下をご覧ください
akif999/kennel

Elements of making text editor

まずは、Go に関わらない部分からお話します。
テキストエディタを開発してみるにあたって、とても重要だと思ったことは以下です。

テキストエディタの挙動を知る(仕様)

私たちプログラマでもテキストエディタの挙動について、あまり意識をしていないことから、
確かな動きを把握していないことが最初の出発点でした。
例えば以下のようなことです。

  • ひたすら左キーを入力し続けた場合カーソルはどのように動くべきか
  • BackSpace を押し続けた場合どのようにウィンドウの文字は削除されるか
  • Enter を押したときカーソルの位置によってどのようにウィンドウの文字が改行されるか

このように列挙すると、ごく当たり前のようなことばかりですが、
これが実装し始めるとどのように動くかわからなくなったりします。
ゆえに、まずは作りたいものがどのようにうごかないといけないか(= 仕様)を知る必要がありました。

テキストエディタのアーキテクチャを設計する(設計)

テキストエディタは小規模なプログラムへの機能追加によっても実現可能ですが、
基礎的な機能を実装する時点でそれなりの規模になります。
よって、ある程度の段階で全体のアーキテクチャレベルでの設計は必要になると考えています。

筆者はこの点については、アーキテクチャモデルの再利用によって設計を実施してみました。
この点については、別の記事に詳しくまとめていますので、ご興味があればご覧ください。

Elements of making test editor by Go

ここからは、Go でテキストエディタを実現するあたっての部分について説明します。

Termbox

Go でテキストエディタを開発するにあたって、ユーザへの入出力部分に以下のライブラリを使用しました。
nsf/termbox-go

この termbox-go は、ターミナルウィンドウへの入出力を行う Interface を提供してくれます。
それにより、テキストエディタを作る際も、その Interface へ入出力を任せ、
私たちはアプリケーションを実装することに注力できます。

Go

そもそも私がテキストエディタを作るにあたって Go を選んだ理由は以下です。

  • 書いていて楽しい
  • テキストエディタに重要なパフォーマンスに優れる
  • シンプルなプログラミングが可能なので、美しい設計を実現しやすい
  • 並行処理が容易である

SourceCode

そして、Go でテキストエディタをプロトタイピングしたときのコードは以下のようになりました。
(まだ、main.go のみで完結させていた時のソースコードです)

package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"

    termbox "github.com/nsf/termbox-go"
)

const (
    Up = iota
    Down
    Left
    Right
)

var (
    undoBuf = &bufStack{}
    redoBuf = &bufStack{}
)

type bufStack struct {
    bufs []*buffer
}

type buffer struct {
    cursor cursor
    lines  []*line
}

type cursor struct {
    x int
    y int
}

type line struct {
    text []rune
}

func main() {
    filename := ""
    fmt.Print(len(os.Args))
    if len(os.Args) > 1 {
        filename = os.Args[1]
    }
    err := startUp()
    if err != nil {
        log.Fatal(err)
    }
    defer termbox.Close()

    buf := new(buffer)
    if filename == "" {
        buf.lines = []*line{&line{[]rune{}}}
    } else {
        file, err := os.Open(filename)
        if err != nil {
            log.Fatal(err)
        }
        buf.readFileToBuf(file)
    }
    buf.updateLines()
    buf.updateCursor()
    buf.pushBufToUndoRedoBuffer()
    termbox.Flush()

mainloop:
    for {
        switch ev := termbox.PollEvent(); ev.Type {
        case termbox.EventKey:
            switch ev.Key {
            case termbox.KeyEnter:
                buf.lineFeed()
            // mac delete-key is this
            case termbox.KeyCtrlH:
                fallthrough
            case termbox.KeyBackspace2:
                buf.backSpace()
            case termbox.KeyArrowUp:
                buf.moveCursor(Up)
            case termbox.KeyArrowDown:
                buf.moveCursor(Down)
            case termbox.KeyArrowLeft:
                buf.moveCursor(Left)
            case termbox.KeyArrowRight:
                buf.moveCursor(Right)
            case termbox.KeyCtrlZ:
                buf.undo()
            case termbox.KeyCtrlY:
                buf.redo()
            case termbox.KeyCtrlS:
                buf.writeBufToFile()
            case termbox.KeyEsc:
                break mainloop
            default:
                buf.insertChr(ev.Ch)
            }
        }
        buf.updateLines()
        buf.updateCursor()
        buf.pushBufToUndoRedoBuffer()
        termbox.Flush()
    }
}

func startUp() error {
    err := termbox.Init()
    if err != nil {
        return err
    }
    termbox.Clear(termbox.ColorWhite, termbox.ColorBlack)
    termbox.SetCursor(0, 0)
    return nil
}

func (b *buffer) lineFeed() {
    p := b.cursor.y + 1
    // split line by the cursor and store these
    fh, lh := b.lines[b.cursor.y].split(b.cursor.x)

    t := make([]*line, len(b.lines), cap(b.lines)+1)
    copy(t, b.lines)
    b.lines = append(t[:p+1], t[p:]...)
    b.lines[p] = new(line)

    // write back previous line and newline
    b.lines[p-1].text = fh
    b.lines[p].text = lh

    b.cursor.x = 0
    b.cursor.y++
}

func (b *buffer) backSpace() {
    if b.cursor.x == 0 && b.cursor.y == 0 {
        // nothing to do
    } else {
        if b.cursor.x == 0 {
            // store current line
            t := b.lines[b.cursor.y].text
            // delete current line
            b.lines = append(b.lines[:b.cursor.y], b.lines[b.cursor.y+1:]...)
            b.cursor.y--
            // // join stored lines to previous line-end
            plen := b.lines[b.cursor.y].text
            b.lines[b.cursor.y].text = append(b.lines[b.cursor.y].text, t...)
            b.cursor.x = len(plen)
        } else {
            b.lines[b.cursor.y].deleteChr(b.cursor.x)
            b.cursor.x--
        }
    }
}

func (b *buffer) insertChr(r rune) {
    b.lines[b.cursor.y].insertChr(r, b.cursor.x)
    b.cursor.x++
}

func (l *line) insertChr(r rune, p int) {
    t := make([]rune, len(l.text), cap(l.text)+1)
    copy(t, l.text)
    l.text = append(t[:p+1], t[p:]...)
    l.text[p] = r
}

func (l *line) deleteChr(p int) {
    p = p - 1
    l.text = append(l.text[:p], l.text[p+1:]...)
}

func (b *buffer) updateLines() {
    termbox.Clear(termbox.ColorWhite, termbox.ColorBlack)
    for y, l := range b.lines {
        for x, r := range l.text {
            termbox.SetCell(x, y, r, termbox.ColorWhite, termbox.ColorBlack)
        }
    }
}

func (b *buffer) moveCursor(d int) {
    switch d {
    case Up:
        // guard of top of "rows"
        if b.cursor.y > 0 {
            b.cursor.y--
            // guard of end of "row"
            if b.cursor.x > len(b.lines[b.cursor.y].text) {
                b.cursor.x = len(b.lines[b.cursor.y].text)
            }
        }
        break
    case Down:
        // guard of end of "rows"
        if b.cursor.y < b.linenum()-1 {
            b.cursor.y++
            // guard of end of "row"
            if b.cursor.x > len(b.lines[b.cursor.y].text) {
                b.cursor.x = len(b.lines[b.cursor.y].text)
            }
        }
        break
    case Left:
        if b.cursor.x > 0 {
            b.cursor.x--
        } else {
            // guard of top of "rows"
            if b.cursor.y > 0 {
                b.cursor.y--
                b.cursor.x = len(b.lines[b.cursor.y].text)
            }
        }
        break
    case Right:
        if b.cursor.x < b.lines[b.cursor.y].runenum() {
            b.cursor.x++
        } else {
            // guard of end of "rows"
            if b.cursor.y < b.linenum()-1 {
                b.cursor.x = 0
                b.cursor.y++
            }
        }
        break
    default:
    }
}

func (b *buffer) updateCursor() {
    termbox.SetCursor(b.cursor.x, b.cursor.y)
}

func (b *buffer) linenum() int {
    return len(b.lines)
}

func (l *line) runenum() int {
    return len(l.text)
}

func (l *line) split(pos int) ([]rune, []rune) {
    return l.text[:pos], l.text[pos:]
}

func (l *line) joint() *line {
    return nil
}
func (b *buffer) pushBufToUndoRedoBuffer() {
    tb := new(buffer)
    tb.cursor.x = b.cursor.x
    tb.cursor.y = b.cursor.y
    for i, l := range b.lines {
        tl := new(line)
        tb.lines = append(tb.lines, tl)
        tb.lines[i].text = l.text
    }
    undoBuf.bufs = append(undoBuf.bufs, tb)
}

func (b *buffer) undo() {
    if len(undoBuf.bufs) == 0 {
        return
    }
    if len(undoBuf.bufs) > 1 {
        redoBuf.bufs = append(redoBuf.bufs, undoBuf.bufs[len(undoBuf.bufs)-1])
        undoBuf.bufs = undoBuf.bufs[:len(undoBuf.bufs)-1]
    }
    tb := undoBuf.bufs[len(undoBuf.bufs)-1]
    undoBuf.bufs = undoBuf.bufs[:len(undoBuf.bufs)-1]
    b.cursor.x = tb.cursor.x
    b.cursor.y = tb.cursor.y
    for i, l := range tb.lines {
        tl := new(line)
        b.lines = append(b.lines, tl)
        b.lines[i].text = l.text
    }
}

func (b *buffer) redo() {
    if len(redoBuf.bufs) == 0 {
        return
    }
    tb := redoBuf.bufs[len(redoBuf.bufs)-1]
    redoBuf.bufs = redoBuf.bufs[:len(redoBuf.bufs)-1]
    b.cursor.x = tb.cursor.x
    b.cursor.y = tb.cursor.y
    for i, l := range tb.lines {
        tl := new(line)
        b.lines = append(b.lines, tl)
        b.lines[i].text = l.text
    }
}

func (b *buffer) readFileToBuf(reader io.Reader) error {
    scanner := bufio.NewScanner(reader)
    for scanner.Scan() {
        l := new(line)
        l.text = []rune(scanner.Text())
        b.lines = append(b.lines, l)
    }
    if err := scanner.Err(); err != nil {
        return err
    }
    return nil
}

func (b *buffer) writeBufToFile() {
    content := make([]byte, 1024)
    for _, l := range b.lines {
        l.text = append(l.text, '\n')
        content = append(content, string(l.text)...)
    }
    ioutil.WriteFile("./output.txt", content, os.ModePerm)
}

プロトタイピングを行った時のコードで、ゴミ混じりですがこれが一番最初に最低限の機能が動くようになったものです。
この時点で300行程度のコード量ですが、以下の機能は実装できていました。

  • 文字の挿入と削除
  • カーソル移動
  • 改行
  • undo redo
  • ファイルの読み書き

Go のシンプルかつ強力な言語仕様のおかげで、私のような経験の浅いプログラマでも、
このようにテキストエディタの開発にチャレンジすることができました。

Roundup

それでは、まとめとして Go でテキストエディタを開発してみて得られたものを以下へ示します。

  • テキストエディタというソフトウェアへの理解が深まった
  • アーキテクチャ設計をする機会が得られた
  • データ構造についてのアイデアのストックができた
  • Go の slice 操作のテクニックが身についた
  • 一般的なテキストエディタの実装について想像の及ぶ範囲が増えた

Reference

最速でGoのAPIサーバを公開する方法

はじめに

Go-Logo_Fuchsia.jpg
Web系の開発やってるとサクッとWebAPIサーバ建てたい・・・!けどめんどくさいことは抜きにしたい!というときありますよね?

コールバック先を指定したいけどいい感じの公開サーバがないときとか・・・
どっかのサーバからリクエスト流してもらって中身確認したい時とか・・・
シンプルに開発中のサーバを一回公開したいとか・・・

この投稿ではサクッとコマンド1発でWebAPIサーバが起動する、丸投げ系テンプレートを紹介します。

動作環境

  1. MacBookPro2016 Mojave
  2. go version go1.10.3 darwin/amd64
  3. Python 3.6.5

事前に

  1. AWSのアカウントを作成する。
  2. EBコマンドを事前にインストールする。

リポジトリ

テンプレート置き場
https://github.com/muff1225/goserver-saisoku

プロジェクトの構成

ファイルの構成は以下のようになっています。

.
├── .elasticbeanstalk
│   └── applicationconfig.yml
├── README.md
├── application.go
├── bin
│   └── application
└── deploy.sh

起動方法

リポジトリからダウンロードもしくはforkして、プロジェクトの中のdeploy.shを実行します。

. deploy.sh

解説

deploy.shの中身は以下のようになっています。

deploy.sh
#!/bin/bash

# building to deploy the application for EB.
go build -o bin/application application.go

# validating have been installed eb command in this machine.
eb --version
if [ $? -ne 0 ]; then
    echo "Please install eb command before running this script.(https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/eb-cli3-install.html)"
fi

# EB status check for the next command.
eb status saisoku
if [ $? -ne 0 ]; then
    # create new one
    eb init goserver
    eb create saisoku
    eb open
else
    # deploy new app
    eb deploy saisoku
    eb open
fi
  1. go build -o bin/application application.goでアプリケーションをビルド
  2. 環境がない場合はeb createでサーバ立ち上げ。(elasticbeanstalk /applicationconfig.ymlを使って初期化します。)
  3. 環境がすでにある場合は、eb deployでビルドしたアプリケーションをデプロイします。

このテンプレートを元にアプリケーションを修正する場合

ElasticBeanstalkのルールとしてgolangサーバを建てる場合、開けるポートは5000番、アプリケーションの実行ファイルはbin/applicationにするようにしなければなりません。
このルールさえ守れば、アプリケーションを修正して、deploy.shでデプロイまで実行可能です。

最後に

このテンプレートを使えば、サクッと自分が自由に使える環境を構成できます。(HTTPSじゃないですけど)
快適な開発をやっていきましょう!

Goでのprotocプラグインのテストの書き方

External article

安全にサーバオペレーションするための、バックアップするだけの簡易コマンドをGoで作った

この記事は Go3 Advent Calendar15日目の記事です。

サーバオペレーション用に作成したツールについて語ります。1

作った経緯

AWS上でサーバ構築のオペレーションをやっていたときに
設定を変更する前に設定ファイルのバックアップをとりわすれることが度々あって、
それを解消したいと思ったからです。

最近でしたらInfrastructure as Codeを推進する動きもあるので
手動でのオペレーション自体を減らしていったほうがいい、とも思います。
が、プライベートでの作業かつサーバ台数も大したことないので、
Ansibleを入れたり、Dockerで全部コンテナ化するまでやるのは流石に(学習コストも含めて)高コストという感覚があります。

なので、Ansibleとかほどでないけれど、設定を更新する前にバックアップを勝手に保存してくれるようなCLIがあれば楽になるんでないかなぁとか考えました。
サーバに配置するものなので、Goで書いてバイナリを生成すれば、簡単に導入できそうだなぁと思って、自分用に作成しました。

作ったもの

sop(safety operation)というツールです。
https://github.com/jiro4989/sop

READMEに記載のとおりgo getでも取得できますし、
Releaseにもあげてるので普通にDLできます。

何ができるのか

使い方はREADMEに書いてることがすべてですが、一応記載。

sopは以下のサブコマンドを持ちます。意図的にLinuxのコマンド名と同じにしてますが、
backupとeditは独自です。
- edit {editor} {targetFile}
- cp
- rm
- backup

使用例

cp

以下のようにコマンドを実行する。

sop cp src.txt dst.txt
sop cp src.txt dst.txt
sop cp src.txt dst.txt

実行結果の確認

ls -la *.txt*
-rw-rw-r-- 1 jiro jiro 5 12月  8 15:13 dst.txt
-rw-rw-r-- 1 jiro jiro 5 12月  8 15:13 dst.txt.2018-12-08_151322
-rw-rw-r-- 1 jiro jiro 5 12月  8 15:13 dst.txt.2018-12-08_151339
-rw-rw-r-- 1 jiro jiro 5 12月  8 15:13 src.txt

見ての通り、cpによって上書きされたdst.txtのバックアップが保存されます。
(現時点では)日付の書式は%Y-%m-%d_%H%M%Sで固定です。

ユーザ、グループ、権限指定の例

権限や所有者を指定しつつコピー。
Linuxコマンドでいうinstallコマンドの簡易版。
installコマンド知らない人以外と多そうなイメージがあります(偏見)。

echo 12345 > dst.txt
sudo chown syslog:root dst.txt
echo 1234567890 > src.txt
sudo sop cp src.txt dst.txt -o root -g syslog -m 0740

結果確認

% ls -la 
合計 20
drwxrwxr-x  2 jiro   jiro   4096 12月 16 02:01 .
drwxrwxrwt 18 root   root   4096 12月 16 02:00 ..
-rwxr-----  1 root   syslog   11 12月 16 02:01 dst.txt
-rw-rw-r--  1 syslog root      6 12月 16 02:01 dst.txt.2018-12-16_020114
-rw-rw-r--  1 jiro   jiro     11 12月 16 02:01 src.txt

こんな感じです。cpの際にパーミッションと所有者を指定したり、
上書き対象の権限を維持したままバックアップを取ったりできます。
その場合は必要な権限が必要なので、だいたいsudoも一緒につけることになると思います(後述)。

ちなみにcp -rとかはできません:sunglasses:

edit

cpでは元になるファイルで上書きしていましたが、こちらはそのファイルをエディタで編集して更新します。
内部的にはexec.Commandしてるだけです。
上書きのアプローチが違うだけでできることはcpと全く同じです。

sop edit vim dst.txtみたいにつかいます。

backup

対象のファイルを更新しません。同じ階層にバックアップだけ保存します。

rm

消すだけです。でもバックアップします。
需要なさそうだなぁとか思いつつも一応用意しました。

作成を通して得られた知見

できることはたったこれだけですが、作成を通して気づいたこととかハマったことについて。

cp -pでコピーする際にファイルの権限を維持したままコピーできます。
backupを保存する実装にcp -pをイメージするようにしていました。
Goで実装を始めたわけですが、Goでファイルコピーをするにはいくつか問題がありました。

  1. そもそもCopyFile的なユーティリティ関数がない
  2. Chownする際に所有者を指定するにはuid, gidを指定する必要がある
  3. Chownには強い権限が必要である
  4. os.FileInfoのUid/Gidを使用しようとするとWindows向けビルドができない

そもそもCopyFile的なユーティリティ関数がない

はい、Goにはファイルコピーのユーティリティ関数がないんです。ioutilにならありそうだと思ってましたが、ないんです。
なので、以下のような感じの関数を自前で定義して実装することになりました:frowning2:

func cp(srcFile, dstFile string, uid, gid int, m ...os.FileMode) error {
    b, err := ioutil.ReadFile(srcFile)
    if err != nil {
        log.Println(err)
        return err
    }

    dst, err := os.Create(dstFile)
    if err != nil {
        log.Println(err)
        return err
    }
    defer dst.Close()

    if _, err := dst.Write(b); err != nil {
        log.Println(err)
        return err
    }

    // rootユーザは 0
    // UID/GIDにマイナス値は使わない
    if 0 <= uid && 0 <= gid {
        if err := dst.Chown(uid, gid); err != nil {
            log.Println(err)
            // chownはできなくてもしかたないのでreturnしない
        }
    }

    if 1 <= len(m) {
        if err := dst.Chmod(m[0]); err != nil {
            log.Println(err)
            // chmodはできなくてもしかたないのでreturnしない
        }
    }

    return nil
}

...はい、色々実装が汚いです:sweat_smile:

UID/GIDを指定しない場合のために-1を指定した場合はchownしないとかいう分岐がありますが、ざっくり調べたところ、値の範囲的にも使わない仕様のはずなので、まぁいいか、と。
Wikipedia - ユーザー識別子

他にも// chownはできなくてもしかたないのでreturnしないとかいうコメントがありますが、こちらは後述します。

Chownする際に所有者を指定するにはuid, gidを指定する必要がある

Goにはユーザ名、グループ名を指定してchownできる関数はなかったと思います。
IDで指定します。Linuxのコマンドのchownコマンドだと名前で指定できるのになー:confused:

でもこのインタフェースをそのままこのCLIのインタフェースにしたくなかったです。
だって、以下の2つでしたら、前述のほうが絶対わかりやすいですので。

sudo sop cp src.txt dst.txt -o root -g root -m 0644
sudo sop cp src.txt dst.txt -o 0 -g 0 -m 0644

0ユーザって誰だよ!!!ってなりますから...:innocent:

なので、コマンドのオプション引数にはユーザ名を受け付けて、
内部でUID/GIDに変換して関数に渡すようにする必要がありました。
なんかいい感じのないかな〜と思ってたら、ありました。
os/userパッケージです。

func LookupGroup(name string) (*Group, error)
func Lookup(username string) (*User, error)

ほー いいじゃないか
こういうのでいいんだよこういうので

使ってるとこ↓

        u, err := user.Lookup(owner)
        if err != nil {
            log.Println(err)
            return err
        }

        g, err := user.LookupGroup(group)
        if err != nil {
            log.Println(err)
            return err
        }

        uid, err = strconv.Atoi(u.Uid)
        if err != nil {
            log.Println(err)
            return err
        }

        gid, err = strconv.Atoi(g.Gid)
        if err != nil {
            log.Println(err)
            return err
        }

Chownの定義とUser, Group構造体

func (f *File) Chown(uid, gid int) error

type Group struct {
        Gid  string // group ID
        Name string // group name
}

type User struct {
        Uid string
        Gid string
        Name string
        HomeDir string
}

uid/gidの型が違う:innocent:
まぁ、こうして実装しました。

Chownには強い権限が必要である

実装してから動作確認してたときにrootユーザ指定でChownしようとしたらerrorが返りました。権限不足だそうで。

あれー、cp -pでrootの権限とか維持したままコピーできてなかったっけかなぁ〜〜〜とか思って、cp -pのほうも試してみました。

sudo touch src.txt
cp -p src.txt src2.txt
ls -la

結果

合計 8
drwxrwxr-x  2 jiro jiro 4096 12月 16 02:47 .
drwxrwxrwt 18 root root 4096 12月 16 02:46 ..
-rw-r--r--  1 root root    0 12月 16 02:47 src.txt
-rw-r--r--  1 jiro jiro    0 12月 16 02:47 src2.txt
                 `..............   .T;(1<<++<1++1+
            ..JdWVVZOIz1lz+Jz?<?Oo,  1~?+<;<<?+?++
 .`      .JWWUC1=zzwOO&&+OUwzwOzti-?(.(J-.v+>+<<;<
     `.JSZXwZZ?jwvTwzCO&+?<+--...--_.?i.?-.?<<<><<
    .dWUXVz;<1CiJC+z=~.JzCd,OZVCuvQ2G(7z{  ?1.?+++
   .HZOI<_/</(v1Jv~.ZC(Z!(MT.XXv3>UNJOo.1    `_!_,
 .gW0v> /<~(viJ?.(v<+<~.MP``1jz1: (dMm-(,}
.XWI<`.~?(v~J^.-jl?<.dHBVn.!.l<>.11yZTMKX_
X0O!`_,~(^.C_-r~dQHHHkwwO?> .zz._zdWkXw+z} .
OZ!  ~.>.J!.C?`.Z3JWkXWWkk>~ OO (dKY!!?Td`` `    `
v` ~-J!.I-J>J .Z+dC```   ?U+ (O}(f~.    (
!  1Z.z!.%.j`..rd% `  `    d. 1zJ. `  v!.      ` .
  .0zzlJ!.(!`_.0d{     ?:` ko~ Cdn~.    ,   ``.JQR
 .ZzwwS:.<{ _ `?vh.`.`    (r`.  1wS+..+vr  `.ZwAVU
.v1jd6O>_( `-  . <U&.-..(0C-.`  (wo?7? .1 .jyz0XXw
+!!.GIzz.+     _ `OOVOvz1<!` .  ~_J!..  (.zOxVOOOw
! J!(/?c(( _ !   .I<<>z<++Jn-2i(2i++(++,(+Jzz+v+vz
 Jv--1o,<+ _` .,OvI1zzkrwwXXOl,OO,C<(-n<j<Iz1Ozz1z
(!`l?ww[<J`!.+GdOv11-((J_~~_(-__``(2  j+d+z+<+z111
t  (._vT.( (JdZ1?!?!   .    .`     ~  .IX;(<<+><1+
< _.t ?v z (Z3>    _   .    .      _  .III_(1<<1(<
+-..(-(1.(..>::    ~`...&JggQm&&-...  (:$<l(>j++(+
.-__+< <z+ (( .. ..dWHMMMHHMMHHHHHMUWe3_D!?C_i<<<z
 l_(2 :.+<~<z! JdHkHHMHMH#MMNMHHB8XXWQM_6.  ?i<(+i
`j(z! _ <+_<R(wWgHMMgggMHMMMMWXQQHNMNMM_}.i `??i<+
 ,sw{_. ~(.`HKHHHHHHHHHMMMHMMHHHHHMMMMM>d.Iz-  _7&
  Hv`~.. (; HMHHHkHkHHHMMM@@HWWWWkHMMMMr}`1<+O,  (
  dv `._`(b XHHHHHbB9UUZTOZOOZUWHHHMM@Mt:/i I+I1.
 .5~  +{ (W,,HMWH91=1z1zz1lzlzldkVMMHMM$~ i 1+1<Ji
.v>. (<: .Uk HXVXz1+<<<><11<<<<1?XZWMHM1   _(,.I<+
Zl_<+<!:  wwl(RXXs!+?<<<<<(z<><<<JXn(MMj;  . .<(+z
+1vz<~ :  jwX>4IzZo.1<><<<>>?+<:(_wX{.M(O.      ?1
zO1+!  :  (kwwn4} ,1-_<1=1+<<+?;<<JNX ,-?z.`  `
zI1<   ~  .d?zOw<. `?yz-.._~<-.-(jdNR.((z+<z-.
O1z _  (  `?} ?1Z+<.:` ?TGage&dXWVY! .}yOv<++11z77
lOv !. (-   1.  <OX,?.    .     .   `.j0Iv<+1=1+++
z1! ._`(l   .1.  (OZi ._??:-......._!+1<+v1?+1zIC+
1z. +> (Z,  `(1.`..1OO.,1tIz&v+zz1OXw>,  .<1zOz<>+
~<111+..ww    (z.~  ?zwn-_!<?><<<<?!`.D.(+1+<!~`_
  -~<<<<+1n   `(?-`  .1OzzOCl11+1vOIwOwzz?`

はい、cp -pで権限が足りないと自分が所有者としてコピーされるみたいです。
その場合も別にエラーではないらしく、echo $?したところ0が返りました。

cpの-pのmanを読んでみたのですが、そういう挙動については記載がなく...。
まぁchownに権限が必要で、多分cp -pも内部的には(多分)cpしてchownしてるんだろうなぁとか考えると、この挙動は至極当然な気がします。
ということで、その挙動にならってGoのChown呼び出しのエラーも握りつぶすようにしました:sweat_smile:
(一応ログだけは出力するようにしましたが)

os.FileInfoのUid/Gidを使用しようとするとWindows向けビルドができない

ファイルのバックアップをする際に、そのファイルの所有者を取得して
ファイルをcpしてchownする必要がありました。
その際、os.FileInfoからUid/Gidを取得するには、以下のようにアクセスする必要がありました。

    fi, err = os.Stat(srcFile)
    if err != nil {
        return nil
    }

    var (
        sys = fi.Sys()
        uid = sys.(*syscall.Stat_t).Uid
        gid = sys.(*syscall.Stat_t).Gid
        m   = fi.Mode()
    )

fi.Sys()はinterface型なので、キャストする必要がありましたが、
それには*syscall.Stat_tを使う必要がありました。この型を使うと、Windowsでビルドできません。まぁシステムコールですからね...。

以下の例でも確認。

% cat main.go 
package main

import (
    "fmt"
    "os"
    "syscall"
)

func main() {
    var (
        fi      os.FileInfo
        err     error
        srcFile = "src.txt"
    )

    fi, err = os.Stat(srcFile)
    if err != nil {
        panic(err)
    }

    var (
        sys = fi.Sys()
        uid = sys.(*syscall.Stat_t).Uid
        gid = sys.(*syscall.Stat_t).Gid
        m   = fi.Mode()
    )

    fmt.Println(sys, uid, gid, m)
}

% go build main.go

% GOOS=windows go build main.go
# command-line-arguments
./main.go:23:15: undefined: syscall.Stat_t
./main.go:24:15: undefined: syscall.Stat_t

当初の予定としては、作ったものはいろんな環境で動かせるようにしたかったので
これはなんとしても回避したい問題でした。
ということで、Windows向けのソースを用意して、ビルド時に参照するソースを切り替えることにしました。

結果的には下記のアプローチで、この問題を解消しました。

  • Windows以外の場合は*.goというファイル名のソースにする。
  • Windows用の場合は*_windows.goというファイル名のソースにする。
  • *.goのソースの先頭に// +build !windowsというタグを埋め込む

これで、go build時に-tags="windows"というタグ情報を渡すことで、
Windowsビルド時は*_windows.goのソースを参照するようになり、コンパイルが通るようにしました。

コード内にタグ情報を埋め込んで色々できるGoならではの芸当ですね:thumbsup:
このソースで使ってます。

ちなみに、このプロジェクトのMakefileではgoxというクロスコンパイル用のGo製CLIを活用させてもらってます。
そちらでもビルド時にgo buildのオプションが指定できるので、以下のようにクロスコンパイルしています。

# VERSION := v$(shell gobump show -r)
VERSION := v1.0.0
DIST_DIR := dist/$(VERSION)
LDFLAGS := -ldflags="-s -w \
    -extldflags \"-static\""
XBUILD_TARGETS := \
    -os="linux darwin" \
    -arch="386 amd64" 
XBUILD_TARGETS_FOR_WINDOWS := \
    -tags="windows" \
    -os="windows" \
    -arch="386 amd64" 

xbuild: $(SRCS) bootstrap ## クロスコンパイル
    gox $(LDFLAGS) $(XBUILD_TARGETS) --output "$(DIST_DIR)/{{.Dir}}_{{.OS}}_{{.Arch}}/{{.Dir}}"
    gox $(LDFLAGS) $(XBUILD_TARGETS_FOR_WINDOWS) --output "$(DIST_DIR)/{{.Dir}}_{{.OS}}_{{.Arch}}/{{.Dir}}"

gobumpはGo製のバージョニングCLI

今後の課題

ということで、いろんな問題に直面して、そして解消して目的を達成しました。
今後の課題はissuesにメモしてるのですが、以下の通りです。

  • 日付の指定にLinuxのdateコマンドと同じ、strftime書式を採用する
  • 変更対象と同じディレクトリ以外にもバックアップ先を指定できるようにする
  • editコマンドで変更がなかった場合(≒差分がない)はバックアップしないようにする
  • 設定ファイルで設定を変更できるようにする

日付の指定にLinuxのdateコマンドと同じ、strftime書式を採用する

Goではかなり特殊なDate書式指定をします。具体的には以下の通り。

    now := time.Now().Format("2006-01-02_150405")

Goに触ったことのない方だと"2006-01-02_150405"!?!?ってなること間違いないです。
Goを使う上では別によくあることなので気にしないのですが、
Goへの理解に明るくない方も使う可能性を考えると、この書式はよろしくないなぁと考えてます。
なので、Linuxのdateコマンドと同じく、strftime書式を採用しようと考えています。
具体的には以下の通り。

date +%Y-%m-%d_%H%M%S

サーバオペレーションをする人が使うことを考えると
サーバオペレーションで頻繁に目にする書式のが扱いやすいでしょうし。

変更対象と同じディレクトリ以外にもバックアップ先を指定できるようにする

実際にプライベートのサーバで設定をしていて/etc/cron.d/foobarの設定を編集していたときに気づきました。読み込むファイルを拡張子とかで制限していない場合に、バックアップファイルも読み込まれてしまうな...と。

include conf.d/*.confみたいな感じで、.confで終わることが指定されているケースならいいのですが、そうでない場合はこの設定ファイルも読み込まれてしまう。
これは非常にマズイです。
なので、設定ファイルと同じディレクトリではなく、別の退避用のディレクトリにコピーできるようにもする必要があるな、と考えています。

イメージとしては/etc/cron.d/foobar/var/backup/etc/cron.d/foobar.20180101みたいに、/からのパス構造を維持したままバックアップするようにしたいです。

editコマンドで変更がなかった場合(≒差分がない)はバックアップしないようにする

はい、やりたいです。Ansibleとかはやってますしね。

設定ファイルで設定を変更できるようにする

前述の設定とかを変更できるようにしたいです。
現状ハードコーディングしてしまってるので...。

まとめ

このCLI作成を通して以下の知見を得ました。

  • os, os/userへの理解が深まった
  • 環境ごとにソースをわけざるを得ない場合のクロスコンパイルの方法を学んだ
  • linuxのuid, gid, chown, chmodの理解が深まった
  • 設定ファイルの読み込まれ方など、考慮すべきことは多岐にわたることを学んだ

丁寧なオペレーションにミスはつきものなので、ミスはツールで予防したいです:helmet_with_cross:
まぁ、rm -rf /とかかました日には復旧不可能ですけどね...:sob:


  1. 「シェルスクリプトでよくね?」と思ったあなた、その指摘は正しい 

GolangのコードをPythonへ移植した話

この記事はGo3 Advent Calendar 2018の16日目の記事です。Golangで書かれたコードをpythonへ移植したときに得られた気づきを共有します。

背景

snakebiteというSpotifyで開発されたpure pythonのhdfsクライアントに copyFromLocal の機能がなかったので自分で実装することにしました。

  • snakebiteの代わりにlibhdfs3などを使えば高速に動作するフル機能が得られるが性能よりも依存関係の少なさを重視したかったのでsnakebiteに機能追加する方針にした
  • HDFSにファイル作成するためのAPI呼び出しシーケンスを把握するのが面倒だったので既存の動いているコードから該当箇所を移植する方向で実装することにした

以上の方針から適当なhdfsクライアントを探した結果、golangで実装されている、colinmarc/hdfsがわかりやすくて必要な機能も実装されているようなのでcopyFromLocalに関連するコードを移植することにしました。

気づき

ここからは、この移植作業から得られた気づきを共有します。実際に、移植したpythonのコードはこちらです。

エラー処理をインターフェイスを変更することなく移植できる

golangでは正常な結果とエラーを多値として返すことが多いと思いますが、pythonではタプルを返すことで、制御構造をほぼ変更することなく移植することができます。

func (f *FileWriter) Write(b []byte) (int, error) {
  /* ... */
  n, _ := s.buf.Write(b)
  err := s.flush(false)
  return n, err
}
class FileWriter(object) {
  def write(b):
    #...
    self.buf += b
    err = self.flush(False)
    return (len(b), err)
}

pythonっぽい書き方ではなくなりますが、書き換えがほぼ発生しないので移植作業にともなう不要なバグ埋めを減らせたように思います。

goroutine / channelのコードはそのまま移植できない

当たり前なんですが、そのまま移植できないのでその場に合わせて書き直す必要があります。例えば、

// Ack packets in the background.
go func() {
  s.ackPackets()
  close(s.acksDone)
}()

//...

func (s *blockStreamWriter)ackPackets() {
  for {
    s, ok := <-s.packets
    if !ok {
      return
    }
    //...
  }
  //...
}

のように、バックグラウンドでackの到着を監視するgoroutineがありました。今回は、パフォーマンスを無視してパケットを送信した直後にackの到着を待つ同期的な処理に書き直しました。

self._send_packet(packet)
err = self._wait_for_acks(packet) # ackパケットの到着を待つ
if err is not None:
  # エラー処理

もちろん、スレッドと非同期IOを使えば完全に移植できると思いますが、とりあえず動かしたかったので今回はパフォーマンスよりも実装のシンプルさを選びました。非同期処理はやはりgolangが使いやすいと感じます。

まとめ

  • golang -> pythonの移植は比較的やりやすい
  • エラー処理をほぼそのまま移植できるので制御構造が大きく変わらなくて済む
  • ただし、goroutine/channelを使っているコードはpythonに対応するような言語機能がないのでスレッドやライブラリを使う必要がある

go-manというツールを作っている話

External article

iOS(Swift)エンジニアがGoでAPI作って感じたこと

External article

Realize が Go 1.11 の Modules で使えない

去年、 [Go] Realizeが便利なので、もう少し仲良くなってみる という記事を公開してから今でもたまにいいねしていただけるのですが、ホットリロードツールとしては本当に realize は使いやすいですよね。

※ もうこの時点で 「いや、こっちのツールのほうがいいよ」 などありましたら、ぜひコメントで教えていただけると泣いて喜びます :pray:

しかし、 Go 1.11 から組み込まれた Modules を使おうとすると realize は動かなくなってしまいます。
本記事では、この原因を探しにいった旅行記になります。

発生した問題

realize init コマンドを打つと、 .realize.yaml というファイルが生成されます。
このファイルを元に、監視するファイルの絞り込みや、変更されたときに Before/After で行うコマンドの指定など、さまざまなオプションを指定することができます。

例えば、.go のファイルを監視して変更がされた場合に、再度 go run ... をさせたい場合は、以下のように設定します(一部抜粋)。

...
schema:
  watcher:
    extensions:
    - go
  commands:
    run:
      status: true
...

ところが、 GO111MODULE=on では realize の起動ができなくなってしまいます。

$ realize start
[09:38:37][MYAPP] : Watching 0 file/s 0 folder/s
[09:38:37][MYAPP] : Build started
[09:38:37][MYAPP] : Build
 exec: not started

この原因を探っていきます。

Issue はあるのか

Go module support

とりあえず、Gopkg.toml を go.mod と go.sum に置き換えたら?

んーそうじゃないんだよな…

Cannot do --run with go1.11 using go mod under windows.

Windows に限った話ではなく、 Mac / Linux でも起きてるよー、とのこと。
ただ、根本的な原因が見えなかったので、直接コードを見ることにしました。

コードを読む

realize.go

とりあえず main パッケージから読み始めます。

まずは realize start してる部分を探します。
CLI パッケージは gopkg.in/urfave/cli.v2 を使ってるみたいですね。

Commands: []*cli.Command{
    {
        Name:   "start",
        ...
        Action: start,
    },

プライベート関数 start を呼んでるので、次はそちら。
いろいろ書かれていますが、最後の return r.Start() の先にありそうですね。

cli.go

見る限り、goroutine で複数のプロジェクトを監視できるようにしてるみたいですね。
複数プロジェクトをまたいだ開発や、そもそも設定でプロジェクトのパスを設定できるため、リポジトリ内には .realize.yaml を含めない、みたいなことも可能だそうです。

// Start realize workflow
func (r *Realize) Start() error {
    if len(r.Schema.Projects) > 0 {
        var wg sync.WaitGroup
        wg.Add(len(r.Schema.Projects))
        for k := range r.Schema.Projects {
            r.Schema.Projects[k].exit = make(chan os.Signal, 1)
            signal.Notify(r.Schema.Projects[k].exit, os.Interrupt)
            r.Schema.Projects[k].parent = r
            go r.Schema.Projects[k].Watch(&wg)
        }
        wg.Wait()
    } else {
        return errors.New("there are no projects")
    }
    return nil
}

次は、 r.Schema.Projects[k].Watch(&wg) 関数を辿ってみます。

projects.go

// Watch a project
func (p *Project) Watch(wg *sync.WaitGroup) {
    ...
    // before start checks
    p.Before()
    // start watcher
    go p.Reload("", p.stop)
    ...

コメントがありがたいですね。 p.Before() この辺が怪しそうです。

func (p *Project) Before() {
    ...
    if hasGoMod(Wdir()) {
        p.Tools.vgo = true
    }
    ...
    // setup go tools
    p.Tools.Setup()
    // global commands before
    p.cmd(p.stop, "before", true)
    // indexing files and dirs
    for _, dir := range p.Watcher.Paths {
        base, _ := filepath.Abs(p.Path)
    ...

hasGoMod(Wdir())p.Tools.vgo が True になっている。
Go1.11 なら go mod ... で良いんじゃなかったっけ…?

Wdir() は Working ディレクトリを返します。
では、 hasGoMod() は?

utils.go

func hasGoMod(dir string) bool {
    filename := path.Join(dir, "go.mod")
    if _, err := os.Stat(filename); os.IsNotExist(err) {
        return false
    }

    return true
}

ははーん。 go.mod があると、 vgo は True になるんですね。
もしや…

tools.go

ありました。
projects.goBefore() 内にある Setup() で以下のように書かれていました。

// Setup go tools
func (t *Tools) Setup() {
    var gocmd string
    if t.vgo {
        gocmd = "vgo"
    } else {
        gocmd = "go"
    }
    ...

    t.Install.name = "Install"
    t.Install.cmd = replace([]string{gocmd, "install"}, t.Install.Method)
    t.Install.Args = split([]string{}, t.Install.Args)
    ...

つまり Go Modules には対応していない みたいです。

まとめ

「お、これは PR チャンスなのでは?」
「いや、そもそもここまで書いてるんだったら、PR 出してマージされましたーぐらいまで話もってこいや」

と思われる方もいらっしゃるでしょう。

しかし、Cannot do --run with go1.11 using go mod under windows. や、他のいくつかの Issue をご覧になった方はすでに把握していると思うんですが、数日前のコメントで

thanks for the support and for the reports, in the last months we had some company changes but in the first trimester 2019 we will release a new version

レポーティングありがとー。2019年の第一期に新しいバージョン出すね。

とのこと。
1.12 から GO111MODULE=on の挙動が標準になるらしい点も踏まえると、もうちょっと待ってみてもいいかな…それでもだめなら他のツールか、もしくは自作してみるのもありかもしれない。

明らかに間違っている情報などありましたら、コメント等でいただけますと幸いです。

[2019/05/12 追記]
@taiba さんより、 Cannot do --run with go1.11 using go mod under windows. #217コメント で解決策が提示されていると教えていただきました。

自分たちが本番でどのように `Wrap(err)` しているかを書きました

External article

Goでプログラムの動的アップデートについてのトリビアです。アドカレ初参加です

External article

Golangで書かれたStatefullなVirtual Web BrowserライブラリのSurfでJavascriptが処理出来るようにしてみたかった

はじめに

これは Go3 Advent Calenderの22日の記事です。
大分遅れて申し訳ありません…。

TL; DR;
出来る限り頑張りましたがottoとwebloopでは実現することが出来ませんでした…。orz
今後ottoとwebloop以外の方法もトライしてみたいと思います。

今回Advent Calender駆動開発で、以前作ったSlackにemojiをアップロードするツールを最新化しようかと考えていましたが、いきなり壁にぶち当たりました。
上記のツールはSlackにemojiをアップロードするAPIがないため、Surfという仮想ブラウザのライブラリを使って、半ば無理やりemojiをアップロードしていたのですが、SlackのUIがアップデートされてから動かなくなっていたのです。
Slackのemojiをアップロードするまでの遷移が

  • ページ内のフォームに入力

から

  • 「絵文字を追加する」ボタンをクリック
  • モーダルダイアログ内のフォームに入力

という遷移に変わったため、それに合わせてプログラムを修正する必要がありました。
最初は単純に考えていて、Surfの brower.Click() でダイアログを出して入力するだけでよいと思っていました。

しかしそう単純ではなかったのです…。
SurfのClick()はJavascriptのイベントの発火はサポートしていなかったのです…。
実際Surfのbrowserのソースを見るとhttpのRequestやResponse、Cookie、DOM用にgoqueryなど、Javascript関連の処理は行っていない様でした。

これは困りました。これが出来ないとそもそもやりたいことが出来ません。
という訳でSurfにどうやったらJavascriptを実行する機能が付けられるか挑戦してみました。

まずは調査

GolangでJavascriptを実行出来る様なヘッドレスブラウザがないかどうか調べていたところ、goqueryのTips and tricksにHandle Javascript-based Pagesという項目がありました。
ここでは以下の2つのライブラリが紹介されていました。

  • Javascriptパーサー otto
  • ヘッドレスブラウザ webloop

この2つでどうにか出来ないかやってみました。

otto

こちらはJavascriptを実行して結果を取得できるというものですが、全てGolangで書かれているというものすごいライブラリでした。
これでページ内のJavascriptを実行すれば出来るはず…と思って調べたのですが、こちらあくまでもjavascriptを実行するためのライブラリとの事で、HTMLを読み込んでDOMを操作するといったことは出来ないとのことでした。
ですが、イシュー等を見た限り、Reactを動かしたりページ内の一部のJavaScriptを動かして必要な情報を取得したりといった使い方も出来るらしいです。

上記の事から今回はottoではHTMLを渡してJavascriptを実行することは難しいと判断しました。

webloop

こちらはGolangからwebkit2を動かすgo-webkit2を使っているヘッドレスブラウザだそうで、URLを読み込んでJSを実行したり出来るそうなので、これならやりたい事が出来るのでは、と思いました。

方法としては、webloopはURIからソースを読み込むだけではなく、HTMLをstringにして読み込ませることが出来る様なので、Surfから必要な時にHTMLを取り出してwebloopでJavaScriptを処理して結果をSurfから返すという流れを考えました。

環境のセットアップ

今回使ったのは(ちょっと古いのですが)macOS Sierraです。

go-webkit2のページを見て必要な依存関係をインストールします。
こちらのイシューを見て、Macの場合以下のコマンドを発行すれば良いことが分かりました。

$ sudo port install webkit2-gtk
$ sudo port install webkit-gtk3

しかし、ずっと前にMacPortsをインストールしたまま放置していた私は既に入っていたものが古かったのか、中々ビルドが成功しませんでした。
結局以下のコマンドで一度全部消してインストールし直しました。

$ sudo port -fp uninstall installed

途中依存関係のあるもののinstallに失敗した場合は、一度失敗したものだけcleanしてinstallしたら成功しました。

sudo port clean 失敗したもの
sudo port install 失敗したもの

テストしてみる

エラーが解消されたら、まずはgo-webkit2のテストをいくつか動かしてみました。
しかしなんだか動きが怪しい…。
VS Codeでテストを実行すると失敗したり、処理が帰って来なかったりと動かない。
ステップ実行すると成功したりと、もう既に不安しかない。

そしてwebloopの方のテストはデバッグ実行でもうまく動かない…。
webloopは諦め、go-webkit2で頑張ることにしました。

既存のテストにないファイルから読み込んでテストするパターンを試してみます。

test_simple.html
<!doctype html>
<html>
    <head>
        <title>test</title>
    </head>
    <body>
        <button id="button1">test</button>
        <div id="target"></div>
        <script>
            document.getElementById('button1').addEventListener('click', function() {
                document.getElementById('target').innerHTML = '<p>good</p>';    
            });
        </script>
    </body>
</html>
simple_test.go
func TestWebView_RunSimpleJS(t *testing.T) {
    webView := NewWebView()
    defer webView.Destroy()

    loadOk := false
    webView.Connect("load-failed", func() {
        t.Errorf("load failed")
    })
    webView.Connect("load-changed", func(_ *glib.Object, loadEvent LoadEvent) {
        switch loadEvent {
        case LoadFinished:
            webView.RunJavaScript(`document.getElementById("button1").click()`, func(result *gojs.Value, err error) {
                if err != nil {
                    t.Errorf("RunJavaScript error: %s", err)
                }
                webView.RunJavaScript(`document.getElementsByTagName("body")[0].outerHTML`, func(result *gojs.Value, err error) {
                    resultString := webView.JavaScriptGlobalContext().ToStringOrDie(result)
                    fmt.Println(resultString)
                    if strings.Count(resultString, "<p>good</p>") > 1 {
                        loadOk = true
                    }
                    gtk.MainQuit()
                })
            })
        }
    })

    f, err := os.OpenFile("test_simple.html", os.O_RDONLY, 0755)
    if err != nil {
        t.Errorf("File open err: %s", err)
    }
    defer f.Close()

    scanner := bufio.NewScanner(f)

    htmlData := ""
    for scanner.Scan() {
        htmlData = htmlData + scanner.Text() + "\n"
    }

    glib.IdleAdd(func() bool {
        webView.LoadHTML(htmlData, "")
        return false
    })

    gtk.Main()

    if !loadOk {
        t.Error("!loadOk")
    }
}

結果無事にステップ実行しなくても成功しました。

API server listening at: 127.0.0.1:16583
dbus[55590]: Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that org.freedesktop.dbus-session.plist is loaded!
dbus[55594]: Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that org.freedesktop.dbus-session.plist is loaded!
<body>
        <button id="button1">test</button>
        <div id="target"><p>good</p></div>
        <script>
            document.getElementById('button1').addEventListener('click', function() {
                document.getElementById('target').innerHTML = '<p>good</p>';    
            });
        </script>


</body>
PASS

本命のHTMLを突っ込んで見る

さて、簡単なHTMLが動作したので本命のページから抜いてきたHTMLを突っ込んでJavaScriptを実行してみたのですが…。
結果は駄目でした。
Javascriptを実行する箇所が待っても返ってきません。

まだ調査の余地はあるのですが、時間も大分過ぎてしまっているので今回はここまでとすることにしました :bow:

やってみて

そもそもscriptタグ内で指定された外部のスクリプトは取得されているのかとか、普通に見る場合は認証が必要なページだが大丈夫なのだろうかなど、色々と検証が不足しているのですが、Webloopがちゃんと動くのかどうかが知りたいところです。
また、認証情報ごと渡してJavaScriptを動かす必要があるページだけ動作させられれば良さそうですが、その辺りも調べてみたいです。
他にもいくつかGolangで動作するHeadless Browserはある様なので、そちらが使えそうかどうか見てみたいと思いました。

P. S.

このイシューの今年の9月辺りの書き込みを見たところ、ドキュメントには乗ってないけど /api/emoji.add にユーザートークンとmultipart formアップロードすればemojiが追加できると書かれていました。
いやーAPIが追加されていないか、もっと早い段階で念のため調べて置くべきでした…。orz
このAPIを使用したプログラムに書き換えれば、取り敢えずやりたい事は出来そうです。
皆さんも時間が経ったらまずは欲しいAPIが提供されてないか確認しましょう。

Fo言語のご紹介

この記事はGo3 Advent Calendar23日目の記事です。

Go3アドベントカレンダーなんですが、 あんまりGoの話が出てきません。
Foの話をします。

Foとは

Foとは、Goに関数型言語の機能を追加したプログラミング言語です。

いくつか特徴的な点があります。ひとつずつ見ていきます。

ジェネリクス

Foでは、ジェネリクスを定義することができます。

type A[T] []T

type B[T, U] map[T]U

type C[T, U] func(T) U

type D[T] struct{
    a T
    b A[T]
}

ジェネリクスを利用して、Mapを定義してみます。

func MapSlice[T](f func(T) T, list []T) []T {
    result := make([]T, len(list))
    for i, val := range list {
        result[i] = f(val)
    }
    return result
}

MapSlice関数は、 func(T) T[]T を引数に受け取って、listの中身をfuncに渡した結果からなる新しいsliceを返します。
RubyやJavascriptではおなじみですね。

今回はTをintとして、funcは以下のように実装してみます。

func incr(n int) int {
    return n+1
}

これを実行してみましょう。main関数は以下のように書けます

func main() {
    fmt.Println(MapSlice[int](incr, []int{1, 2, 3}))
}

どうやって実行するのかですが、今回はThe Fo Playgroundを利用します。
実行すると、incrされたlistが表示されますね。

次は、Tをstringとしてみます。
このようなコードを書きました。

package main

import "fmt"


func MapSlice[T](f func(T) T, list []T) []T {
    result := make([]T, len(list))
    for i, val := range list {
        result[i] = f(val)
    }
    return result
}

func past(n string) string {
    return n + "ed"
}

func main() {
    fmt.Println(MapSlice[string](past, []string{"want", "watch", "point"}))
}

実行結果は [wanted watched pointed] のようになります。

カリー化

もうちょっとFunctional Programmingっぽいことをしたいので、カリー化を実装してみます。

このようなコードを書きました。

package main

import "fmt"

func curry2[P1, P2, R](f func(P1, P2) R) func(P1) func(P2) R {
  return func(p1 P1) func(P2) R {
    return func(p2 P2) R {
      return f(p1, p2)
    }
  }
}

func main() {
  add := curry2[int, int, int](
    func(a, b int) int {
      return a + b
    },
  )

  incr := add(1)
  fmt.Println(incr(1))
  fmt.Println(incr(10))
}

curry2関数は、f func(P1, P2) R を受け取って、 func(P1) func(P2) R を’返す関数です。
ここでは、incrがfです。

ちなみに、そもそもGoは、関数を引数で受け取る・関数を戻り値で返す関数を定義することができるため、
がんばればGo単体でもカリー化を実装することは可能です。

まとめ

その他の例はexamplesにあります。
Goでもジェネリクスがサポートされるという話がありますね。
Foは開発がストップしてしまっている雰囲気があるのですが、面白い試みだと思ったため紹介しました。

君は全てのケースに備えているか? 〜コード静的解析のススメ〜 #golang

動機

こんなカンジの型と定数リストがあったとします。

a/a.go
package a

type TestKind int

const (
    TestKindHoge TestKind = iota
    TestKindFuga
    TestKindPiyo
)

いわゆる列挙というか区分みたいなヤツですが、これに対してswitch文とか書きますよね。

sample.go
switch v {
case a.TestKindHoge:
    // do something
case a.TestKindFuga:
    // do something
case a.TestKindPiyo:
    // do something
}

で、実装後に誰か他の開発者が区分が追加したとします。

a/a.go
package a

type TestKind int

const (
    TestKindHoge TestKind = iota
    TestKindFuga
    TestKindPiyo
    TestKindBosukete // Add!
)

その時にswitch文のcaseの追加が漏れていて意図しない挙動になったりすることってないでしょうか。

switch v {
case a.TestKindHoge:
    // do something
case a.TestKindFuga:
    // do something
case a.TestKindPiyo:
    // do something
default:
    panic("unexpected")
}

などとしておけば最悪実行時に検知はできるかもですが、可能ならビルド時に検知したいですよね。

静的解析

go vetgolint みたいなコンパイル前のコード静的解析で検知することができるのでは?
と思い立ちました。

https://godoc.org/golang.org/x/tools/go/analysis

上のパッケージを使用するとGoの静的解析ツールが作成できます(しかもGo1.12からgo vetで呼び出せる様になるらしい)。

ちょうど日本におけるGo静的解析の(というかGoの)伝道師 @tenntenn さんが超わかりやすい記事を書いてくれました!

Goにおける静的解析のモジュール化について
モジュール化された静的解析の実装を追ってみよう

本記事ではanalysisパッケージについての詳細は割愛し、上記を参考に作成したツールを紹介します。

allcasesチェッカー

てな訳で作りました。

allcases

インストール

$ go get github.com/knightso/allcases/cmd/allcases

使い方

$ allcases [-flag] [package] 
  • flagは全て go/analysis から引き継いだもので、必須ではないです。興味ある方は allcases -help でチェックしてください
  • package指定はgo tool準拠です

アノテーション

switch文の前に // allcases というコメントをつけることで、評価する値の型の定数全てのcaseが網羅されるかをチェックします。

sample.go
// allcases
switch v {
case a.TestKindHoge:
    // do something
case a.TestKindFuga:
    // do something
case a.TestKindPiyo:
    // do something
}

出力例↓

/src/sample/sample.go:36:2: no case of a.TestKindBosukete

最後に

個人的にコード静的解析は今後ブームになるのではないかと予想していたのですが、go/analysisパッケージとgo1.12のgo vet組み込みの話を聞いてますます確信に近づきました。(というか自分が遅れてるだけですでに流行ってるってことかな?^^;)

go vetやgolintなどに用意されている汎用的なチェッカーを使うのみでなく、各プロジェクトに特化したカスタムlinterやanalyzerを開発者が気軽につくることで生産性を上げる開発手法が流行る気がしてます。

さいごにもう一つ、今回のツールを作るのに、やはり @tenntenn さん作の commentmap.Analyzer を利用させて貰いました。ありがとうございました。 :bow:
https://github.com/tenntenn/comment
アノテーションコメントの解析を簡単に行うことができるユーティリティAnalyzerです。Analyzer.Requiresフィールドに設定することで利用できます(^^)

grapi : #golang で interface driven かつボイラプレートに悩まされない API 開発

Go Conference 2018 Spring にて, Go で快適に Web API 開発をするための CLI + ライブラリである grapi について話した.
本記事では,grapi で典型的なAPIをどう実装するかのワークフローとともに,grapi の特徴や思想を紹介する.

記事中では grapi v0.3.2 について扱う.

grapi の特徴・やること

  • 開発者は gRPC IDL でスキーマを定義し,gRPC server を実装する
  • Ruby on Rails を意識した file generator
    • rails new に対応した grapi init
    • rails gに対応した, grapi g NAMEgrapi generate NAME
      • Google API Design Guide に準拠
      • protobuf のスキーマや gRPC server の実装スケルトンも生成できる
      • → 開発者は server の中身の実装に集中できる
  • grapi server を実行すればデフォルトで :3000application/json な http server が立つ
    • ちょっと設定を変えれば application/grpc も話せるようになる

プロジェクト新規作成

grapi init APP_NAME でプロジェクトのボイラプレートが生成される.APP_NAME. を入れるとカレントディレクトリに生成される.
いまは2分くらいかかってるが,これは dep ensure がこっそり2回走っているのが原因.

asciicast

生成されるファイルは以下.

  • api
    • protobuf のスキーマ定義と生成されたコードが入る
  • app
    • アプリケーションコードが入る
    • cmd/server に合わせて pkg/server にしたら良かったってちょっと後悔している)
  • cmd
    • エントリポイント(main パッケージ)が入る
    • cmd/CMD_NAME があるとき grapi CMD_NAME でコマンドが起動できる(後述)
  • tools.go
    • gex が利用する,ツール間利用マニフェストファイル
    • protoc や grapi のプラグインはこのファイルで管理されている
  • Gopkg.{toml,lock}
    • dep
    • Go Module 安定したらそっちを柄用にしたい
  • grapi.toml
    • grapi の設定ファイル
    • protoc のプラグインや引数などもここに記述する
:) % tree -I "vendor|bin"
.
├── Gopkg.lock
├── Gopkg.toml
├── api
│   └── protos
│       └── type
├── app
│   ├── run.go
│   └── server
├── cmd
│   └── server
│       └── run.go
├── grapi.toml
└── tools.go

7 directories, 6 files

ここまでは大したことはしていない.

API 定義

asciicast

API スキーマの生成

grapi の API は gRPC の IDLgoogle.api.Http で HTTP へのマッピングを記述したもので定義される.
grapi g service でそのスキーマ定義 + Go の server 実装の雛形が生成できる.また,grapi g scaffold-service を利用すると Google API Design Guide の Standard Methods に則った形式の,いわゆる RESTful っぽいスキーマ定義を生成することができる.

ここは完全に rails g (scaffold_)?controller を意識している.
普通に gRPC server を実装しようとすると「.proto を書いて」「頑張って protoc の引数を組み立てて実行して」「生成された interface を実装した Go のオブジェクトを作る」までやったあとにようやくサーバの実装に取り掛かれる.
grapi g (scaffold)?-service だとその準備をすべてすっ飛ばして最低限の雛形ができるので,結構きもちいい.

API スキーマの更新

grapi による API 開発における開発者のメンタルモデルは,unary な gRPC server を実装しているときとだいたい同じ,大まかにつぎの3ステップのループになるはず(厳密にはテスト書いたりいろいろあるけど,そのへんはよしなに補完してほしい).

  1. .proto ファイルの更新
  2. protoc の実行
  3. 実装の変更

grapi はこのうち「protoc の実行」について面倒を見る.grapi protoc を叩くことで .proto ファイル全てに対してそれぞれ必要なプラグインすべてを実行してくれる.これは先述した grapi.toml に記述されているとおりに実行される.

# `grapi init` で生成される `grapi.toml` の一部
# 標準では `protoc-gen-go`, `protoc-gen-grpc-gateway`, `protoc-gen-swagger` の3つが利用される

[protoc]
protos_dir = "./api/protos"
out_dir = "./api"
import_dirs = [
  "./api/protos",
  "./vendor/github.com/grpc-ecosystem/grpc-gateway",
  "./vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis",
]

  [[protoc.plugins]]
  name = "go"
  args = { plugins = "grpc", paths = "source_relative" }

  [[protoc.plugins]]
  name = "grpc-gateway"
  args = { logtostderr = true, paths = "source_relative" }

  [[protoc.plugins]]
  name = "swagger"
  args = { logtostderr = true }

API 実装

とりあえずモックデータを返す

asciicast

grapi init で生成したプロジェクトでは,grapi server で API サーバを起動できる.
しかし,grapi g (scaffold-)?service でファイル生成した直後は,どれだけ curl しても返事がない(正確には 501 Not Implemented が返ってくるが).

$ curl localhost:3000/todos                                                                                     
{"code":12,"message":"Not Implemented"}

まず最初に,grapi のサーバに生成されたサーバ実装を登録する必要がある.

diff --git a/app/run.go b/app/run.go
index c9c9016..46ac33c 100644
--- a/app/run.go
+++ b/app/run.go
@@ -1,6 +1,7 @@
 package app

 import (
+   "github.com/izumin5210-sandbox/todoapp-grapi/app/server"
    "github.com/izumin5210/grapi/pkg/grapiserver"
 )

@@ -9,7 +10,7 @@ func Run() error {
    s := grapiserver.New(
        grapiserver.WithDefaultLogger(),
        grapiserver.WithServers(
-       // TODO
+           server.NewTodoServiceServer(),
        ),
    )
    return s.Serve()

ここで再度 curl すると,微妙に結果が変わる.

$ curl localhost:3000/todos                                                                                     
{"code":12,"message":"TODO: You should implement it!"}

これは grapi g (scaffold-)?service で生成された Go のコードがそういう実装になっているため.

// `grapi g scaffold-service todo` で生成された Go のコードの一部
// rpc の実装はすべて `codes.Unimplemented` を返すようになっている

func (s *todoServiceServerImpl) ListTodos(ctx context.Context, req *api_pb.ListTodosRequest) (*api_pb.ListTodosResponse, error) {
    // TODO: Not yet implemented.
    return nil, status.Error(codes.Unimplemented, "TODO: You should implement it!")
}

ここのコードをとりあえずモックを返すように書き換えてみる.

diff --git a/app/server/todo_server.go b/app/server/todo_server.go
index eab2a9c..f71bb38 100644
--- a/app/server/todo_server.go
+++ b/app/server/todo_server.go
@@ -26,8 +26,14 @@ type todoServiceServerImpl struct {
 }

 func (s *todoServiceServerImpl) ListTodos(ctx context.Context, req *api_pb.ListTodosRequest) (*api_pb.ListTodosResponse, error) {
-   // TODO: Not yet implemented.
-   return nil, status.Error(codes.Unimplemented, "TODO: You should implement it!")
+   return &api_pb.ListTodosResponse{
+       Todos: []*api_pb.Todo{
+           &api_pb.Todo{TodoId: "1", Title: "Write Go 4 Advent Calendar at 2018/12/05", Done: true},
+           &api_pb.Todo{TodoId: "2", Title: "Write Go 2 Advent Calendar at 2018/12/15", Done: true},
+           &api_pb.Todo{TodoId: "3", Title: "Write Go 3 Advent Calendar at 2018/12/20", Done: true},
+           &api_pb.Todo{TodoId: "4", Title: "Write Go 3 Advent Calendar at 2018/12/20", Done: false},
+       },
+   }, nil
 }

 func (s *todoServiceServerImpl) GetTodo(ctx context.Context, req *api_pb.GetTodoRequest) (*api_pb.Todo, error) {

これで改めて curl をすると,それっぽい JSON が返ってくるようになる.

$ curl localhost:3000/todos | jq .                                                                              
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                    
                                 Dload  Upload   Total   Spent    Left  Speed                                      
100   315  100   315    0     0  45559      0 --:--:-- --:--:-- --:--:-- 52500                                     
{                                                                                                                  
  "todos": [                                                                                                       
    {                                                                                                              
      "todo_id": "1",                                                                                              
      "title": "Write Go 4 Advent Calendar at 2018/12/05",                                                         
      "done": true                                                                                                 
    },                                                                                                             
    {                                                                                                              
      "todo_id": "2",                                                                                              
      "title": "Write Go 2 Advent Calendar at 2018/12/15",                                                         
      "done": true                                                                                                 
    },                                                                                                             
    {                                                                                                              
      "todo_id": "3",                                                                                              
      "title": "Write Go 3 Advent Calendar at 2018/12/20",                                                         
      "done": true                                                                                                 
    },                                                                                                             
    {                                                                                                              
      "todo_id": "4",                                                                                              
      "title": "Write Go 3 Advent Calendar at 2018/12/20"                                                          
    }                                                                                                              
  ]                                                                                                                
}

grapi が面倒を見てくれるのはここまで: API Desing Guide に従ったコード生成と protoc のラップ,あとはコマンドの実行とビルドだけ.
実際のアプリケーション開発はここからが本番になるが,その「本番」に至るまでの瑣末事を肩代わりするのが grapi の仕事になる.

おまけ

DB をつなぎ込む

実際にデータ永続化とかをするときは,*sql.DB などのコネクションなりクライアントなりを fooServiceServerImpl struct にもたせると良い.
ちょっと古い & 内容が異なるサンプルだけど,Go + grpc-gateway でつくる JSON API 速習会 @ Wantedly で利用したサンプルリポジトリの sqlx 導入 PR が参考になると思う.

アプリケーションが大きくなってくると server に *sql.DB を直接もたせるんじゃなくて,DAO や repository などの抽象化パターンの導入を検討するといい.

gRPC を使う

grapi init した直後のプロジェクトは普通に localhost:3000application/json で話しかけると返事をしてくれる.
これは内部で2つのサーバを動かすことで実現されている.

  • gRPC server
    • unix domain socket(デフォルトだと tmp/server.sock)を listen
  • grpc-gateway server
    • tcp の :3000 を listen
    • gRPC client デフォルトだと tmp/server.sock にある gRPC server を見に行く

これらのサーバがどこを listen するかは,以下に挙げる grapi の起動オプションで切り替えることが可能.

当然だけど,grpc-gateway を経由せず gRPC server と直接 application/grpc で通信することもできる.

なので,「今後 gRPC を導入していきたいのでアプリケーションレイヤでの知見がほしい・IDL を先行利用していきたい」みたいなユースケースでとりあえず grapi を使っておいて,サーバ・クライアント・インフラすべての準備が整ったタイミングで gateway を剥がす…という使い方も可能(というか,まさにその用途で使いたくて grapi を作った).

既存のパッケージ・ツールとの比較

ginecho, net/http.Server を使った実装との比較のメリットとしては,やはり json.Unmarshal をしなくていいことに尽きると思っている,ちゃんとした struct の形でパラメタが渡ってくるのが一番嬉しい.これはコード生成ベースでやることの大きな強みである.

goa は同じくコード生成ベース・interface driven な開発ができるパッケージである.これに対する強みとしては前述した通り「gRPC 移行への前段階として利用できる」ほか,「protobuf は実装言語に依存しない IDL で記述するので,多言語・プラットフォームからの利用も簡単」というのは大きいはず.

生 gRPC server との比較としては…,「gRPC をいつでも使いはじめられる環境」が既にあるのなら grpc-gateway を通さずに使うべきである.grpc-gateway を使わないとしても,grapi のもつ Google API Design Guide に則ったボイラプレート生成は有効に使えると思う.また,この「gRPC をいつでも使いはじめられる環境」というのは実は結構難度が高い(インフラ的な障壁, スキーマ共有どうする, 生成コードはどう扱う, etc).なので,そこまですぐに用意できない・だけど近い内に gRPC を導入しておきたい という状況のときには grpc-gateway を内包する grapi は強い味方になると思う.

ちなみに,「IDL から(サーバ・クライアントの)コード生成」というのは microservices architecture かつサービス数が増えれば増えるほど欲しくなってくる.なので,そういう状況に置かれてる現場では gRPC の投入を見越しつつ grapi の採用を検討してもらえると嬉しい.

雑感

grpc-gateway は直感的でわかりやすいんだけど,むだに json -> pb 変換が挟まるので用途によってはパフォーマンスが気になるかもしれない.
grpc-gateway 以外の,でも gRPC は使わないアプローチとしては

  • reflection protocol をうまく使ったやり方(e.g. mercari/grpc-http-proxy)を考える
  • protobuf をしゃべる http client を生成 & protobuf をそのまま proxy するだけの server を作る
  • protobuf IDL から http.Server & HTTP クライアントを生成

等があるかもしれない.
一方で,protobuf & gRPC まわりはかなりエコシステムが発展してきているので,最終的には生で gRPC を利用できるに越したことはないはず.

Browsing Latest Articles All 25 Live