Software Engineer Blog

エンジニアブログです。技術情報(Go/TypeScript/k8s)や趣味の話も書くかもです。

エンジニアとして学びたいこと(歴2年目)

概要

エンジニアとして、どのあたりを学んでいこうかなぁとの備忘録

項目

Golang

最近ずっと利用しているメイン言語です。仕事上ではJavaを利用することが多いですが、 書いている時間も量もそろそろ超えてくるのではと思っています。

好きなところはこんな感じです。

  • ビルドしたバイナリがあればどの環境でも動かしやすい点
  • 型あり
  • Httpサーバーの起動しやすさ
  • プログラマーならたぶん誰でも読める
  • ドキュメントが読みやすい

学びたいこと

  • パッケージの構成方法
    • 自由に書けてしまってどこに何を書くかが明確になっていない
    • パッケージの切り方
    • 各ファイル内のinterface,strcut,methodの構成
  • 周辺ツールの利用方法
    • ツールが充実しているのことで、きっちり使い方を抑えたい(pprofとか)
  • 実務上でのコードレベル
    • OSSのライブラリ等を読んでみる等ですかね
  • 体系的な知識
    • Webでの情報がメインのため、体系的な知識が身についてない疑惑
    • 書籍でまとまったものがあれば一度読むのも良いかも

次のステップ

クラウド

実務できっちり利用できていない分、プライベートで学んでおかないと遅れが目立ってしまう印象です。

学びたいこと

  • GAE/Goの動作方法
  • k8sについて
  • GCP主要サービス理解
    • そもそものサービスについてあまり知らない
  • AWS(ざっくり)

次のステップ

低レイヤー

Goならわかるシステムプログラミングを読んでいて感じましたが、低レイヤーの知識が圧倒的に足りないと感じます。このあたりのコンピュータサイエンスについて、きっちり抑えておきたいです。
低レイヤー周りを最適化したコードを実装できるように、理解を深めたいです。

学びたいこと

  • Unixについて
  • プロセスについて
  • メモリ管理について(GC等)
  • 通信について

次のステップ

まとめ

ざっくりと思い当たることについて、並べて書いてみました。自分の整理にはなったかなとは思います。
学ぶのに良い情報をお持ちの方がいたら、ぜひ教えていただけると幸いです。
まだまだ、手を動かしてコードを書く経験値が足りなさすぎるので、手はとにかく日々動かし続けていきたいとは思っています。

GoSNS

概要

github.com

AmazonSNS likeな、簡易メッセージングサーバーを練習がてら書いてみました。
(Amazon SNSちゃんと使ったことないので、ぜんぜん違うかもしれないですが…)
モデルは、Pub/Subを意識しています。

機能概要

  • Channel登録
  • Channelに対して購読登録
    • 購読手段はSlackのWebHook一択(Mail等の対応も検討中)
  • 新規Channelの開設
  • Handshakeリクエス

簡易API Doc一覧

メルカリ製のgo-httpdocを利用してドキュメント生成しました。

github.com

I/F

基本POSTリクエストでサーバーとやり取りをします。
POSTのBodyに下記のRequest構造のJSONを書き込んでリクエストします。

Channel登録 (/meta/channel)

新規に開設したい、Channelを登録します。

Request

名前 概要
channel String 登録したいchannel名

サンプル

{
  "channel": "golang"
}

Response

JSON構造

名前 概要
channel String 登録したchannel名
successful String 登録成否
error
(optional)
String エラー

購読登録 (/meta/subscribe)

購読の登録をします。
現在は、Slack通知(WebHook URL)を用いての手法にのみ対応しております。

Request

名前 概要
channel String /meta/subscribe
client_id String ID(現在は適当な文字列)
subscriptions Array(String) 購読したいchannelのリスト
method Method 購読手法

Method

名前 概要
method String 購読手段(下記選択)
- slack
webhook_url String Slack WebHookURL (slack選択時必須)

サンプル

{
  "channel": "/meta/subscribe",
  "client_id": "MRAjWwhTHcgagka",
  "subscription" : [
    "/golang"
  ],
  "method" : {
    "format": "slack",
    "webhook_url": "https://hooks.slack.com/services/XXX"
  }
}

Response

JSON構造

名前 概要
channel String /meta/subscribe
successful String 購読成否
client_id String ID
subscriptions Array(String) 購読したchannelのリスト
error
(optional)
String エラー

Topic登録 (/topic)

Channelに対して、Topicを登録します。

Request

名前 概要
channel String Topic登録するchannel名
data String Topic内容
{
  "channel": "/golang",
  "data" : "*Update GAE Go1.8*"
}

Response

文字列

文字列
成功 ok
失敗 not found channel

内部実装

どのように実装をしているか、メモ書き程度に残しておきます。

Pub/Sub Model

PublisherとSubscriberの関係は、1Topicに限ると1対多の関係です。
PublisherはどのSubscriberに送信するかといった情報には関与しません。
Publisherはtopicを送るだけ、Subscriberは送られたTopicを購読するだけといった構成です。

データ保持

購読データの保持は、JSONファイルにて行っております。
(DB等の利用がベタ-かと思いましたが、ファイルが一番楽だったので選びました)
ファイルI/Oを呼び出しごとに発生させたくなかったため、内部でキャッシュを持たせています。 キャッシュとファイルは常に同期している想定です。
ただ、キャッシュから時間でデータが破棄されることもあり、キャッシュにない場合はファイルを見にいくようにしてます。

ユーザー認証

完全に未実装です。認証なくPOSTが送れたら自由にデータの書き換えができます。

テスト

go-httpdoc に記法にならって記述しています。
API I/Fの部分以外で書けていないところは、順次書き足したいです。

パッケージ管理

depを利用しています。 管理ファイルがtomlファイルになってて驚きましたが、問題なく使えています。

利用しているライブラリ一覧

感想

Goを触り始めて、初めてある程度ましな成果物を作成しました。
AmazonSNSのドキュメントを読んで、メッセージングサーバーの面白さを感じ、とりあえず自分でも作ってみたといった感じです。

書いてて感じたことですが、パッケージ構成の方法 がよくわからなかったです。
パッケージの切り方や、同一パッケージ内でのファイル分割の方法等で、いまいち方針がなく何度も再構成しました。
他の方のソースを読んで、このあたりは勉強していこうかなと思います。
総じて、楽しく書けたのでGoはかなり自分にあっている気がします。

sync.Poolの挙動について(Golang)

概要

標準パッケージのsync.Poolを利用して、ファイルの中身を常にメモリ上(=キャッシュ)におきたかったのですが、動作が不定のためうまく利用できなかった話です。

Go version

% go version
go version go1.8.1 darwin/amd64

sync.Pool概要

概要としては、以下の認識です

  • オブジェクトを一時的に、保持するための機能
  • 再利用されるオブジェクトをキャッシュする利用法
  • 複数のgoroutineで安全に利用可能
  • copy禁止(noCopystructが埋め込まれている)

メソッド

Get

func (p *Pool) Get() interface{}

  • Poolよりitemを取り出す
  • 取り出されたitemはPoolから削除
  • Putで入れた値とGetで取り出す値の関係を考慮すべきでない

Put

func (p *Pool) Put(x interface{})

  • Poolに値を追加する

動作確認

下記のコードを利用して、Getした際とPutした際の挙動を確認してみました。
https://github.com/midorigreen/gopool/blob/master/main.go

package main

import (
    "fmt"
    "net/http"
    "sync"
)

var pool = sync.Pool{
    New: func() interface{} {
        return "init"
    },
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/pool", poolHandler)
    mux.HandleFunc("/pool/change", poolChangeHandler)
    http.ListenAndServe(":8888", mux)
}

func poolHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("----------------------------------")
    fmt.Println("/pool")
    fmt.Println(get().(string))
    fmt.Println("----------------------------------")
}

func poolChangeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("----------------------------------")
    s := get().(string)
    fmt.Println("/pool/change")
    pool.Put(s)
    fmt.Println("-- changed --")
    fmt.Println("----------------------------------")
}

func get() interface{} {
    s := pool.Get().(string)
    cpS := s
  // Getした値を再度Poolに戻している
    pool.Put(s)
    return cpS
}

出力例

実際の出力例です。
上記コードより、get()で呼び出した際は、Get→Putとしているため
同じ値がGet()で取得されて欲しいところです。
ですが、一度/pool/changeAPIを叩いた後も"init"が出力されていることから、
Get→Putで値を戻したところで、必ず同じ値をGet()してくれるわけではなさそうです。

----------------------------------
/pool
init
----------------------------------
----------------------------------
/pool/change
-- changed --
----------------------------------
# ↓ これ以降はすべて "change handler" が出力されていて欲しい
----------------------------------
/pool
change handler
----------------------------------
----------------------------------
/pool
init
----------------------------------
----------------------------------
/pool
change handler
----------------------------------
----------------------------------
/pool
init
----------------------------------
----------------------------------
/pool
change handler
----------------------------------
----------------------------------
/pool
init
----------------------------------
----------------------------------
/pool
change handler
----------------------------------
----------------------------------
/pool
change handler
----------------------------------
----------------------------------
/pool
init
----------------------------------

感想

Callers should not assume any relation between values passed to Put and the values returned by Get.
→ Putで入れた値とGetで取り出す値の関係を考慮すべきでない

とあるように、Put()で入れたからGet()で必ず入れた値が取得できるわけではないことを念頭に置いて開発しないといけないようです。
ファイルの中身をキャッシュしておいて、書き換え時は更新等を行いたかったのですが、 更新をしたデータをPut()で突っ込んでも返ってくるとは限らなくなってます。
結論、あまり良い使い方ではなさそうです。(一意にキャッシュを保てないので、実データと差分がでてしまうため)

補足

@nobonoboさんにも指摘頂きました。

参考

Bayeux プロトコルの仕様について

はじめに

Pub/Sub Messagingを勉強するにあたって、Bayeuxプロトコルでの実装があるとのことだったので、Bayeux プロトコル日本語訳を読んでメモを取ってみました。
枯れた仕様(検索であまり当たらない)な気もしていますが、せっかく読んだので公開しようかと思います。

Bayeuxプロトコル概要

  • Webサーバー間でメッセージを非同期にやりとりするためのプロトコル
  • メッセージは名前付きチャネルを経て、ルーティング及び送達される
    • サーバ <=> クライアント
  • ajaxを利用したサーバープッシュ型のテクニック=「comet」
  • 準拠要求
    • 全てのMUST及びREQUIREDを満たし、かつ、全てのSHOULDを満たすものは、"完全準拠(unconditionally compliant)"
    • 1つでもSHOULDを満たさないモノは「条件付き準拠(conditionally compliant)」
  • ワード定義
    • メッセージ
      • サーバーとクライアント間で交換されるJSONオブジェクトである
    • イベント
      • Bayeuxプロトコルを用いて送られるアプリケーション固有のデータである
    • チャネル
      • 名前付きのイベント送達先(と送信元)。イベントはチャネルに対して投稿され、受信者はチャネルからこれを受け取る

HTTPプロトコル

  • リクエスト/レスポンス型のプロトコル
  • クライアント -> サーバー
  • サーバー -> クライアント
  • 基本的にサーバー -> クライアントへはクライアントの要求なしに通信を走らせない
  • Bayeuxでは、双方向の非同期通信を走らせるためサーバー・クライアント間で複数コネクションをサポートしている
    • 通常アクセス用コネクション
    • ロングポーリング用コネクション
  • (MUST NOT)Bayeuxプロトコルも、サーバーが全てのアプリケーションに対して処理を行うために3本以上のコネクションをはらない

様々なルール

  • (MUST) クライアントはブラウザのシングルオンポリシーを尊守しなければならない
    • シングルオンポリシー = JSからはそれがダウンロードされたサーバー以外への接続は許可されない
  • 2コネクション処理
    • (MUST) Bayeux実装はHTTPのパイプライン処理を制御し、リクエスト順を保持して実行しなければならない
  • ポーリング
    • レスポンスを受け取った後、メッセージをサーバーに対して送信する
      • 次のメッセージを受け取れるようにする処理
    • Bayeux実装は、ロングポーリングと呼ばれる形式のポーリングをサポートする必要がある
  • 接続ネゴシエーション
    • 接続の際、コネクション/認証/を交換して合意するネゴシエーションが行われる
    • その際、ハンドシェークメッセージがかわされる
  • クライアントの状態
-------------++------------+-------------+----------- +------------
State/Event  || handshake  | Timeout     | Successful | Disconnect
            ||  request   |             |   connect  |  request
            ||   sent     |             |  response  |   sent    
-------------++------------+-------------+----------- +------------
UNCONNECTED  || CONNECTING | UNCONNECTED |            |
CONNECTING   ||            | UNCONNECTED | CONNECTED  | UNCONNECTED
CONNECTED    ||            | UNCONNECTED |            | UNCONNECTED
-------------++------------+-------------+------------+------------
  • Bayeuxにおける名前や識別子に用いられる文字はBNF(バッカスナウア)記法
// BNF記法
alpha    = lowalpha | upalpha
lowalpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" |
           "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" |
           "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
upalpha  = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" |
           "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" |
           "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
digit    = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" |
           "8" | "9"
alphanum = alpha | digit
mark     = "-" | "_" | "!" | "~" | "(" | ")" | "$" | "@" 
string   = *( alphanum | mark | " " | "/" | "*" | "." )
token    = ( alphanum | mark ) *( alphanum | mark )
integer  = digit *( digit )
  • チャネル
    • URI絶対パス部分の形式で記述される名称
    • /meta/で始まるチャネル名は予約されている
      • (SHOULD NOT) リモートのクライアントは購読するべきでない
    • パターン指定
      • 単一のセグメントを表す"*"
      • 複数のセグメントにマッチする"**"
    • /service/チャネル
      • サービスチャネルに投稿されたメッセージは他のクライアントに伝送されない
      • (SHOULD) サービスチェネルに行われた購読要求を記録管理すべきでない
    • メッセージ
      • フィールド名と値名が並んだ順不同なJSONオブジェクトとしてて定義される
      • 必ず一つの"channel"フィールドを持たねばならない
channel_name     = "/"  channel_segments
channel_segments = channel_segment *( "/" channel_segment )
channel_segment  = token

メッセージフィールド定義

説明
channel - メッセージの配送先および配送元を示す
- リクエスト: 配送先
- レスポンス: 配送元
supportedConnectionTypes - /meta/handshakeへ送受信の際に利用
- どの転送タイプを用いるか決める
- (MUST) long-polling,callback-polling等が存在
clientId - クライアントをユニークに識別するもの
- /meta/handshake,投稿メッセージ以外の全てのメッセージに付与が必須
id 全てのメッセージに付与可能
timestamp - ISO 8601形式(YYYY-MM-DDThh:mm:ss.ss)
- GMTで表記
successful -リクエストの成否
-/meta/*系のレスポンスに必須
error エラー

メタメッセージ定義

handshake

クライアントは、/meta/handshakechannelに対してメッセージを送ることで接続ネゴシエーションを開始する

handshake request (MUST field)

field 説明
channel /meta/handshake という値
version クライアントで処理されるバージョン
supportedConnectionTypes 接続タイプの文字列

handshake response

field 説明
channel /meta/handshake という値
version クライアントで処理されるバージョン
supportedConnectionTypes クライアント・サーバー共にサポートする接続タイプ
clientId クライアント固有のID
successful true/false

connect

クライアントは、/meta/connection channelにメッセージを送ることでコネクションを開始する。

connection request

field 説明
channel /meta/connection という値
clientId クライアント固有のID
connectionType 接続タイプ

connection response

field 説明
channel /meta/connection という値
successful true/false
clientId クライアント固有のID

subscribe

チャネルへの登録を行うために、/meta/subscribeにリクエストを行い、配信登録を行う。

subscribe request

field 説明
channel /meta/subscribe という値
clientId クライアント固有のID
subscription 購読するチェネル名/チャネルパターン/配列

subscribe response

field 説明
channel /meta/subscribe という値
successful true/false
clientId クライアント固有のID
subscription 購読するチェネル名/チャネルパターン/配列

イベントメッセージ定義

イベントメッセージ投稿

channelに対して、イベントを投稿する処理のこと

publish request

field 説明
channel 投稿するchannel名
data JSONオブジェクトの形をしたメッセージ

publish response

field 説明
channel 投稿したchannel名
successful true/false

イベントメッセージ配信

channelからクライアントへイベントを配信する

field 説明
channel 投稿するchannel名
data JSONオブジェクトの形をしたメッセージ

ポーリング

long-polling

特徴

  • 配送すべきメッセージが届くまでコネクションをオープンにし続ける
  • 伝送遅延を最小化
  • フェールセーフとして、classicalな数秒ごとのポーリング処理を行う
  • POSTメッセージのボディで伝送される
  • 形式
    • application/x-www-form-urlencoded
    • text/json

参考

GolangのSort処理について

GolangのSort処理

GolangのSort処理について、まとめました。
(// package sort と記載があるサンプルコードは、Golang本体のソースコードです)

Sort

Sort Interface

Golangでは、structのソートを行うため、sort.Interfaceを実装する必要があります。
(実際はGo1.8以降、下記の sort.Slice() を利用して、ソートすることができるようになりました。参考)

// sort.Interface
type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}
メソッド 特徴
Len() int 配列・リストの要素数
Less(i, j int) bool i < j を満たすかの真偽値
Swap(i,j int) iとjの要素を入れ替える

Sortの方法

標準パッケージで対応している型は下記になります。

  • float64
  • int
  • string

ここでは、ソートの方法をint配列をもとにみていきます。

sort.Ints(a int[])

Ints() メソッドは、int[]型をソートするメソッドです。
これは、昇順(increacing)にソートされます。

nums := []int{4, 3, 2, 10, 8}
sort.Ints(nums)
fmt.Println(nums)

// output: [2 3 4 8 10]

内部では、 IntSlice 型のstructにつめかえを行い、Sort関数を呼んでいます。
(sort.Interfaceを実装するルールは同じ)

// package sort

// Ints sorts a slice of ints in increasing order.
func Ints(a []int) { Sort(IntSlice(a)) }


// IntSlice
// Len,Less,Swapを実装したstruct

// IntSlice attaches the methods of Interface to []int, sorting in increasing order.
type IntSlice []int

func (p IntSlice) Len() int           { return len(p) }
func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p IntSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

sort.Reverse(data Interface) Inteface

Reverse(data Interface) は、sort.Interface型のソート方法を逆順に修正する関数です。
実際に内部で行っていることは、下記の2点です。

\1. reverse structstructを詰め替える
reverse struct は、 sort.Interface を埋め込んでいます。

// package sort

type reverse struct {
    // This embedded Interface permits Reverse to use the methods of
    // another Interface implementation.
    Interface
}

// Reverse returns the reverse order for data.
func Reverse(data Interface) Interface {
    return &reverse{data}
}

\2. Lessメソッドの引数をスワップ
reverse struct は、実装を参照したところ、 Less(i,j int) bool のみを再実装しています。
これにより、比較条件を逆にすることで、逆順ソートを実現しています。

// Less returns the opposite of the embedded implementation's Less method.
func (r reverse) Less(i, j int) bool {
    return r.Interface.Less(j, i)
}

sort.Search(n int, f func(int) bool) int

nの長さの配列内でf()=true となる最小のindexを返却します。
→ 配列内で指定のvalueがあるかどうかを調べる関数です。

特徴

  • 条件を満たさなかった場合、nが返却
  • 一致を探すことも、ある数値以上の値を探すことも可能
  • binary search(二分探索)で検索
    • ソートされた配列に対して実行しないと正確な結果が返却されない
  • 下記はSearchのラッパー関数
    • func SearchFloat64s(a float64, x float64) int
    • func SearchInts(a int, x int) int
    • func SearchStrings(a []string, x string) int
// package sort

// binary sort の実装
func Search(n int, f func(int) bool) int {
    // Define f(-1) == false and f(n) == true.
    // Invariant: f(i-1) == false, f(j) == true.
    i, j := 0, n
    for i < j {
        h := i + (j-i)/2 // avoid overflow when computing h
        // i ≤ h < j
        if !f(h) {
            i = h + 1 // preserves f(i-1) == false
        } else {
            j = h // preserves f(j) == true
        }
    }
    // i == j, f(i-1) == false, and f(j) (= f(i)) == true  =>  answer is i.
    return i
}

// Int ラッパー関数
func SearchInts(a []int, x int) int {
    return Search(len(a), func(i int) bool { return a[i] >= x })
}

利用サンプル

nums := []int{4, 3, 2, 10, 8}
x := 8

// 事前にソートしておく
sort.Ints(nums)
// search asc
index = sort.Search(len(nums), func(i int) bool {
  return nums[i] >= x
})

if index < len(nums) && nums[index] == x {
  // nums[index] = 8 = x
} else {
  // x は nums[]中に存在しなかった
  // だが、indexはnums[]の中にxをインサートする位置にある
}

sort.Slice(slice interface{}, less func(i, j int) bool)

渡したsliceを、ソートする関数。
less関数で定義された順にソートされる。

特徴

  • Go1.8以上
  • sort.Interfaceを実装していなくてもソートが可能となっている
  • stable sort(安定ソート)を保証していない
    • sort.SliceStableを利用することで保証される

(参考)実際のソート処理の中身について

sort.Sort(data sort.Interface) は渡されるデータによってソートの種別変えています。
流れとしては、基本的にクイックソートを利用して、条件を満たした際の他のソート手法に切り替わるといった挙動です。

  • ヒープソート
    • 条件: log(n+1)の整数値の2乗の深さとなった場合
      • 右に1ビットずつシフト演算して0になるまでの回数の2乗が最大の深さ(maxDepth)
      • maxDepthが0となったときに、ヒープソートに切り替わる
// package golang

// Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached.
// package golang

// Use ShellSort for slices <= 12 elements

まとめ

Golangのsortパッケージについて、学びました。
実際のソート処理の中身までは、深く追求しませんでしたが、
ソートを行う利用方法については触れられたかなと思います。

参考

公式ドキュメント
golang の sort インタフェース難しい問題が解決した

golang.tokyo #6 に行ってきました

概要

golang.tokyo #6 に参加しましたので、レポートを記載いたします。
(会場は、DeNAさんでお寿司(食べ損ねた)とお酒をご用意いただいておりました。)
下記は各セッションのまとめです。

Gopher Fest 2017

@tenntenn さん

スライド

概要

サンフランシスコで開催されるGoSFが主催のイベントの参加レポートです。
セッションの中のひとつ The state of Go のお話

The state of Go

Go1.9での仕様変更や標準ライブラリの改善についてのお話
(余談ですが、tip.golang.orgで最新のmasterのドキュメントが見れるそうです)

  • Go1.9は5/1にコードフリーズ済み
  • リファクタリングを安全にする方法
  • Goで安全にリファクタできるのか?
    • 定数の場合は問題なし
    • 関数はラップして引き継げば問題なし
    • 型の場合→ 問題有り
      • 型を作る → メソッドが引き継げない
      • 埋め込み → キャストができない
      • Go1.9でAlias導入
  • 言語仕様変更
    • Alias を作成できる
      • 型のエイリアスを定義できる
      • こんな感じで type A = http.Client
      • 完全に同じ型
      • キャスト不要
      • エイリアスの方ではメソッドを定義できない
  • 標準ライブラリ変更
    • math/bits ビット演算に便利なライブラリ
    • syc.Map スレッドセーフなマップ
    • html.templateがより安全に利用可能に
      • errorを返却するようになる
    • os.Exec
      • 環境変数が重複の際、一番後ろのものを優先する
        • ソース中で上書き可能
  • go testの改善
    • vendorを無視するようになった

感想

Go1.9にあたっての改善点のお話がメインとなりました。
@tenntennさんもおっしゃってましたが、 go testvendor配下を
みなくなる修正はなかなか良いですね。

初めてGolangで大規模Microservicesを作り得た教訓

Yuichi Murata さん

スライド未公開(2017/06/01)

概要

GAE/Goで大規模なMicroservices(30くらい)を作った際の、教訓のお話

  • いきなりつかってみたため、失敗談が中心
  • 構成
    • GAE/Go
    • Gin/Echo
    • Microservices構成
  • 結論
    • 困ったときはGoの哲学にそったシンプルなアプローチを取る
    • Goを過信せずパフォーマンスに気を配る

教訓

1. フレームワークにこだわらない

  • Railsの経験から良いフレームワーク探しをこだわって行った
  • Gin
    • 良い点
      • シンプル
      • App Engine とのインテグレーション有り
      • Framework Context vs App Engine Context
        • gin.ContextからApp Engine Contextに派生させられることで解消
    • 困った点
  • Echo
    • 良かった点
    • 困った点
      • 開発が盛んなことにより設計が書き換わる
        • バージョンを固定することで対応(アップデートできない..)
  • まとめ
    • Goでは、net/httpパッケージで十分な部分もある
      • 足りない部分はライブラリを利用
    • フレームワークを利用しなくても統一的なコードとなる
      • 統一的なコードを書くためのフレームワークだったが、Goなら標準で統一的となる

2. Interfaceを尊重する

  • 独自のエラー型を定義
    • こちらの型にerrorを寄せる
      • 毎回キャストするのが面倒のため
  • Nilに型がある問題発生
  • まとめ
    • Interfaceが定義されたものはそのまま利用するほうが自然に実装できる

3. regex compile/ reflectionが遅い

  • バリデーション
    • JSON Schemaを利用
  • GolangでのJSON Schema
  • パフォーマンステストで問題発覚
    • バリデーションの有無でパフォーマンスに顕著な差が出る
    • pprof を利用して実行を解析
  • 問題点の解消
    • Regexコンパイルを毎回行う実装
      • キャッシュすることで解消
    • reflectionを多用していた
      • 利用しないように修正
    • go-jsvalを利用すればさらに早くなった
  • まとめ
    • regex/reflectionのコスト高を意識する
    • regex compile / reflectionはとにかく重い

4. 非対称暗号は遅い

  • 認証・認可処理
    • 非対称鍵の処理が重い
      • opensslと比べcypto/rsaが貧弱
        • 一桁程度の差が生じる
      • cgoを利用したopensslのバインディングも存在
        • GAEでは利用不可
  • 荒業で解消
    • 非対称鍵の処理署名処理をPHPにして外出し
    • httpリクエストのほうが早い

感想

GAE/Goでサービスを作る際の問題点がいくつか聞けました。
サービスでの利用はこれからあるかもなので、参考になりました。
またnilに型がある問題は、まだあたったことがないので知れて良かったです。

[LT]ゲーム開発に欠かせないあれをしゅっとみる

ゴリラのアイコンの方(@Konboi)

スライド

概要

  • CSVの話
  • カヤックではデータの受け渡しとしてCSVを利用している
    • マスターデータ等
  • CSV困る時
    • カラムとデータの関連性が見づらい
    • 空欄がつらい
    • DBにインポートする系はその際にわかる
    • 調査系で利用する時つらい
  • csviewer
    • csvをDBのテーブルを検索するように参照できる
  • ライブラリ
    • sliceflag
      • 同一オプションで複数の値を受け取りたい
      • 普段のflagの利用方法と変わらない
    • tablewriter
      • データをいい感じにテーブル表示してくれる

[LT] Go Review Commentを翻訳した話

@knsh14 さん

スライド

概要

Go Code Review Commentを読もう
(→ 読みます)

  • Go Code Review Commentとは
    • コードレビューをする際に見るべき箇所をまとめたもの
    • Effective Goの簡略版
  • 翻訳してみました(qitta)
  • 良かった点
    • 正解のパターンを勉強できるところが効率が良い
  • 内容
    • コードの見た目を改善
      • ツールでなんとかしよう
      • golintは優秀
    • コメントや文章の体裁
    • Tips系
      • より良いパターンを記載
    • 設計の指針
  • まとめ
    • 良いドキュメントの翻訳は英語の勉強に最適

[LT] ScalaからGo

James さん

スライド未公開(2017/06/01)

概要

  • 関数型開発はGoでできるか?
  • 関数型開発のコンセプトは利用できるか?
    • 利用できる!!
  • 関数型開発とは?
    • 副作用がない開発
    • 副作用あり
      • テストしにくい、バグの原因
    • 部分適用
  • Goは初心者が入りやすく会社での導入がスムーズに行える点が良い

[LT] Crypto in Go

概要

  • Goにおける暗号アルゴリズムの利用
  • AES
    • 固定長でしか利用できない
  • AES + Padding + HMac
    • 行数があって面倒
  • AES + GCM
    • 機密モード + 認証モードがひとつに
    • とってもシンプル

まとめ

各セッションとLTについて、ざっくりと記載いたしました。
詳細知りたい際は、スライドが公開されるはず(ほぼされてますが)ですのでそちらをご参照ください。
全体的に有意義な発表が聞けて楽しかったです。
開催いただきありがとうございました。(次回も参加したいです)

JUnit4を利用したJavaのテスト概要

概要

JUnitを利用した、Javaユニットテストを作成する際の基本的な部分について記載しました。
JUnit実践入門の内容 + 経験から記載しました。

テストコード基本

ユニットテスト対象物

テストクラスに対する、テスト対象物はテストクラスのコードのみとします。
そのため、外部オブジェクトの作成等は基本的にモック化します。
(外部クラスのコードのテストは、外部クラスのテスト上で行えば良いとの考え方です。)
またテストメソッドの対象物ですが、基本的に publicメソッド を対象とします。
privateメソッドは、publicメソッドを正確にテストすることで、同時にテストされていなければ、ならないと考えているためです。

各命名と役割

命名 役割 備考
XXXTest テストクラス名
xxx_条件_結果 テストメソッド - xxx()メソッド
- @Testアノテーションを付ける
sut テスト対象オブジェクト変数名 - テスト対象のオブジェクトを明確化するために同一名称で宣言する。
- System Under Testの略
setUp() テストごとの前処理 - 各テスト実行ごとの前処理を記載
- @Before アノテーションを付ける
tearDown() テストごとの後処理 - 各テスト実行ごとの後処理を記載
- @After アノテーションを付ける
setUpClass() テストクラスごとの前処理 - テストクラス内のテストが一つでも実行される前の処理を記載
- @BeforeClass アノテーション記載
- public static methodで宣言
tearDownClass() テストクラスごとの後処理 - テストクラス内のテストが全て実行された後の処理を記載
- @AfterClass アノテーション記載
- public static methodで宣言

テストのフェーズ

  1. 事前準備 = set up
    下記を行います。

    • テストデータ作成
    • DBの接続
    • モックの作成
    • モックからの返り値の設定
    • Testクラスに渡すオブジェクトの作成
    • Testクラスのオブジェクト化
    • ..etc
  2. 実行 = exercise
    テスト対象のメソッドを実行します

  3. 検証 = verify
    メソッドの実行結果を検証します。
    メソッドの返り値が期待値通りかを判定するフェーズです。

  4. 後処理 = tear down
    テスト実行後の後処理を行います。

    • テストデータの破棄
    • DBの切断

Assert

テスト結果を検証するための方法についてです。
検証とは基本的に、テスト結果が期待値通りかを確かめるフェーズとなります。

  • Assertは、org.junit.Assert.assertThat() を利用
     - `assertThat` 一つで基本的に事足りる
     - static importを行っておく
    
  • Matcher
    • org.hamcrest.CoreMatchers
      • is() equalsメソッドによる比較
      • nullValue() nullであることを検証する
        • 利用する際は、 is(nullValue()) と利用する
      • not() 評価値を反転させる
    • org.junit.matchers.JUnitMatchers
      • hasItem() Iterableインタフェースを実装したクラスに、期待値が含まれているか検証
      • hasItems() 複数指定可能
    • BaseMatcher<> を継承することで、カスタムMatcherが作成できる

サンプルコード

上記までの内容を踏まえた、テストサンプルコードです。

public class TargetClassTest {
  @Before
  public void setUp() {
    // テストごとの共通の前(初期化)処理
  }

  @After
  public void tearDown() {
    // テストごとの共通の後処理
  }

  @Test
  public void hasUserName_ユーザー名称をもつ場合_trueを返却() throws Exception {
   // set up
   /** テスト対象のオブジェクトの変数名は sut とする **/
   TargetClass sut = new TargetClass)();

   // exercise
   boolean actual = sut.hasUserName();

   // verify
   /** 英語の構文 "assert that actual is expected" を意識する**/
   asseertThat(actual, is(true));
  }

(追記) Exception発生時のテスト

  /**
  * Exception発生時のテスト
  */
  @Test(expected = NullPointerException.class)
  public void fetchUserName_ユーザー名称を取得に失敗_exception発生() {
   // set up
   /** テスト対象のオブジェクトの変数名は sut とする **/
   TargetClass sut = new TargetClass)();

   // exercise
   /** 下記メソッド実行時にException発生 **/
   Result actual = sut.fetchUserName();
  }
}

テスト作成にあたっての他用語

Fixture

事前準備にて、設定する情報のことで、下記2パターンのFixtureの設定方法があります。

  • inline set up
    • 各テストメソッドごとに、fixtureのset upを行う
    • simpleに設定を記述すれば良い
    • コードが長くなり、可読性が悪くなりがち
  • implicit set up

パラメータ化テスト

テストに対してパラメータを設定したい際に、利用します。

Theories を利用して実現します。

  • Theories
    • @RunWith(Theories.class) をクラス宣言の前に宣言
      • テストランナーの一つ
    • @Theory テストメソッドのアノテーション
    • @DataPoint パラメータ
// サンプルコード
@RunWith(Theories.class)
public class TargetClassTest {
  @DataPoint
  public static int PARAM_1 = 1;

  @DataPoint
  public static int PARAM_2 = 2;

  public TargetClassTest() {
    // 初期化処理
  }

  @Theory
  public void testCase(int x) throws Exception {

  }
}

Rule

テストをプラグイン的に拡張できる機能のことです。
テストに関係なく、初期化+後処理をしたい場合 に便利です。
publicなfiledにアノテーションをつけて利用します。

// サンプル
@Rule
public Timeout timeout = new Timeout(100);
  • 処理はテストごとに実行される
    → テストごとに実行したい共通処理をRuleとしてまとめられる
  • @ClassRule にてテストClassごとに1回のRuleも作成可能
  • カスタムルールの作成 方法1
    • org.junit.rules.TestRule インターフェイスを継承する
    • Statement apply(final Statement base, Description description) をOverrideする
      • 引数で渡される Statement base が各テストメソッドのイメージ
      • base.evaluate() を実行することでテストが実行される
    • これを利用することでテストに共通の前後処理を定義することができる
public class HogeRule implements TestRule {

  private void before() {}
  private void after() {}

  @Override
  public Statement apply(final Statement base, Description description) {
    // new Statement()することで実際のテストを拡張している
    return new Statement() {
      // 前処理
      before()
      
      // テスト実行 (@Before -> テスト実行 -> @After)
      base.evaluate();

      // 後処理
      after()
    }
  }
}
  • カスタムルールの作成 方法2 (← こちらのほうが効率よく作れます)
    • 上記の設定を、事前に行っているクラス(ExternalResource)を利用
    • ExternalResourceを継承して作成
    • 利用する際は、before()after()をOverrideして、前後処理を記載する
    • Rule内で利用する変数はprivate fieldに宣言しておき、コンストラクタで受け取る
// 上記のサンプルコードをすでに実装した抽象クラス
public abstract class ExternalResource implements TestRule {
    public Statement apply(Statement base, Description description) {
        return statement(base);
    }

    private Statement statement(final Statement base) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                before();
                try {
                    base.evaluate();
                } finally {
                    after();
                }
            }
        };
    }

    /**
     * Override to set up your specific external resource.
     *
     * @throws if setup fails (which will disable {@code after}
     */
    protected void before() throws Throwable {
        // do nothing
    }

    /**
     * Override to tear down your specific external resource.
     */
    protected void after() {
        // do nothing
    }
}

モック

モックとは、テスト対象クラス以外のクラスを擬似的に作成して、対象外クラスのメソッドの 返り値を設定する事ができます。
テスト対象クラス に絞ったテストを行うために重要な要素です。

  • org.mockito.Mockito ライブラリを利用する
    • JavaDoc
      • mockの利用方法について詳細が記載されている
    • モックの作成 mock(TargetClass.class)
    • 返り値の設定 when(sut.exec(anyInt(), any(Date.class))).thenReturn(obj)

データベースのテスト

(手法が確立できていないため一時保留)

  • H2 Databaseの利用
    • ピュアJavaSQLデータベース
    • インメモリ or ファイルの2種類の作成方法がある
    • 組み込みモード、サーバーモード、ミックスモードで動作
    • jarファイル1つ(1.5MB)動作
    • JDBCサポート
  • DBUnit
    • @Ruleとして、作成することでDB接続周りを一手に引き受けられる
    • org.dbunit.AbstractDatabaseTester を継承する

コードカバレッジ

カバレッジの種類

  • C0(命令網羅)
    • プログラム中に定義された命令が1回以上実行されたかを測定
    • line coverageと同等
    • 基準
  • C1(分岐網羅)
    • すべての分岐(if)を1回以上実行したかを測定
    • if条件の true or false のどちらも通す必要がある
  • C2(条件網羅)
    • すべての条件を1回以上実行したかを測定
    • if条件のtrueになる全ての条件とfalseになるすべての条件を見る必要がある

カバレッジ測定ツール

# テスト実行 + カバレッジ測定
% mvn clean jacoco:prepare-agent test jacoco:report

# カバレッジレポート参照
% target/site/jacoco/index.html

おわりに

JUnitを利用した、ユニットテストに関してざっくりとした記事を書きました。
昨今は、マイクロサービス化の利用等でますますユニットテストの価値が上がってきているかと思います。
(テストがないコードはレガシーとまで言われるかもしれないですね。。)
テストを何のために書くのかや、どういったテストが良いテストなのかについては特に記載しませんでした。
そのあたりは自分で考えていただいて、テストの有用性に対して正確な理解を個人でしてもらえればと思います。
また、自分で利用したことがないため、テストランナーについてあまり記載できていませんでしたので、 学んだ際に追記できればと思います。

参考文献

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)