Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Go言語でTDDする勉強会 第6回(2020/02/05) 発表メモ

Go言語でTDDする勉強会 第6回(2020/02/05) 発表メモ

connpass

Go言語でTDDする勉強会 (初心者大歓迎!)

Slack

Join Weeyble on Slack. ( weeyble.slack.com )
事前に登録をお願いします

channel: #go

参加者への質問

前提

  • ローカル環境でGo言語が動作すること

本勉強会の管理者

今回の発表者


前回までの資料

第2回, 第3回 は ハンズオン手順毎に 細かく箇条書き
-> 面倒になってきたので、 第4回 予習メモ 以降 は概要のみ


教材の説明, ディレクトリ構成, TDD について, Test Double (Stub, Mock, Spy) について

前回の資料を参照


今回の進め方

本 gist 資料 で概要を解説
 ↓
教材を ハンズオン形式で 解説 (時間無いので出来るだけコピペで済ませます)

1時間くらい経ったら途中、休憩を入れます(5分〜10分)

12. Select
 ↓
13. Reflection
 ↓
(番外編). 「Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る」 第8章(8.3 8.4) 解説


前提知識: http.HandlerFunc

http.HandlerFunc の定義

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}
  • HandlerFunc は 関数 func(ResponseWriter, *Request) 」 型 を 基底 とする 型
    • -> 関数 func(ResponseWriter, *Request)HandlerFunc 型 に 変換可能
  • HandlerFuncServeHTTP メソッド を持つ
    • ServeHTTP メソッド は 関数自身(=f) を呼び出す

前提知識: http.Handler interface

http.Handler の定義

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
  • HandlerFuncServeHTTP メソッド を持つ
    • -> Handler interface を満たす
    • -> Handler 型 の変数に 代入可能

 

HandlerFuncHandler の コード例: Go Playground

 

12. Select

12. Select

要件

  • 2個のURL を引数に取る Racer 関数 の test を行う

Racer 関数 の 2つの仕様

  1. 2つ の URL に対して http.Get して 応答が返ってきた 時間が短い方の URL を返す
  2. 2つ の URL 共に timeout 以内に 応答が返ってこない場合は error を返す

 

問題点: 外部サービスに依存

  • http.Get で 実際のWebサイトにアクセスしている

解決策: モック HTTP サーバー (httptest.Server) に対して test

  • httptest.NewServer 関数
    • func NewServer(handler http.Handler) *Server
    • Handler を引数 に取って *Server を返す 関数

racer_test.go

fastServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
}))

fastURL := fastServer.URL
// この URL に対して `http.Get` 実行
  • test では httptest.ServerURL に対して http.Get する

defer

  • 補足: A Tour of Go: Defer 参照
    • > defer へ渡した関数の引数は、すぐに評価されますが、
    • > その関数自体は呼び出し元の関数がreturnするまで実行されません
  • 補足: A Tour of Go: Stacking defers 参照
    • > defer へ渡した関数が複数ある場合、その呼び出しはスタック( stack )されます
    • > 呼び出し元の関数がreturnするとき、 defer へ渡した関数は LIFO(last-in-first-out) の順番で実行されます

v1 の racer_test.go

// makeDelayedServer の中で httptest.NewServer 実行
fastServer := makeDelayedServer(0 * time.Millisecond)

// Server 生成のすぐ直後に defer で Close() を記述
defer fastServer.Close()
  • keep the instruction near where you created the server (server を 作成した場所の近くに 指示を保管します)

v1 終了

 

Synchronising processes. Always make channels

  • 2つの URL を順番に http.Get するのではなく、 同時に実行 する
  • http.Get(url) 部分を goroutine 化
    • (1) 関数 内で最初に channel 作成
    • (2) goroutine 起動
    • (3) 関数 から channel を 戻り値 で返す
      • 下記 ping 関数 は 戻り値 を返して 直ぐに終了 する
    • (4) 「処理」は goroutine 内で 非同期で 行う. 処理が終了したら goroutine 内で channel を close

v2 の racer.go

func ping(url string) chan struct{} {
	ch := make(chan struct{})         // (1)
	go func() {                       // (2)
		http.Get(url)
		close(ch)                 // (4)
	}()
	return ch                         // (3)
}

Synchronising processes. select

v2 の racer.go

func Racer(a, b string) (winner string) {
	select {
	case <-ping(a):  // (1)
		return a              // (3)
	case <-ping(b):  // (2)
		return b              // (3)
	}
}
  • 上記 Racer 関数 の処理の順番
    • (1) 1つ目の url(a) を 引数として ping 関数 を呼び出し.
      • ping 関数 の 戻り値 の channel が直ぐに返ってくる.
      • casechannel からの 受信 を待ち受け
    • (2) 1つ目の url(b) を 引数として ping 関数 を呼び出し.
      • (1) と同じ
    • (3) case で (1) と (2) の channel に対して goroutine から 先に送信された値 を受信
      • 戻り値 として a または b を返す
  • select
    • 複数の channel を待機
    • The first one to send a value "wins" and the code underneath the case is executed. (値を送信する 最初のものが「勝ち」case の下のコードが実行されます)
    • 補足: @tenntenn さん作のスライド: 「 Goroutineと channelから はじめるgo言語 」 の p21 〜 参照
    • 補足: A Tour of Go: Select 参照
      • > 複数ある case のいずれかが準備できるようになるまでブロックし、準備ができた case を実行します
      • > もし、複数の case の準備ができている場合、 case はランダムに選択されます
    • 補足: 以下の処理 でも select を使用します
      • 以下の 「タイムアウト処理 (case <-time.After(10 * time.Second):)」
      • 15. Context「 context の キャンセル処理 (case <-ctx.Done():)」

v2 終了

  • 補足: ping 関数の呼び出しにどれだけ時間がかかっているか確認
func ping(url string) chan struct{} {
	defer trace(fmt.Sprintf("url=%s", url))()  // 追加

	// 中略
}

func trace(msg string) func() {
	start := time.Now()
	log.Printf("enter %s", msg)
	return func() {
		log.Printf("exit %s (%s)", msg, time.Since(start))
	}
}

 

Timeouts: Write the test first

  • serverA, serverB 共に「処理が Timeout(=10秒) を超えたら error を返す 」という test code を追加
serverA := makeDelayedServer(11 * time.Second)
serverB := makeDelayedServer(12 * time.Second)
// 中略
_, err := Racer(serverA.URL, serverB.URL)
  • serverA (11秒) も serverB (12秒) も 処理が10秒を超える ため error が返る

Timeouts: time.After

  • case <-time.After(10 * time.Second): を追加
  • Racer 関数 の 戻り値 に error 追加
    • タイムアウト していない 場合は nil を返す
    • タイムアウト した 場合は error (fmt.Errorf("timed out waiting for %s and %s", a, b)) を返す
      • 1つ目 の 戻り値 は "" (string の ゼロ値)
func Racer(a, b string) (winner string, error error) { 
	select {
	case <-ping(a):
		return a, nil
	case <-ping(b):
		return b, nil
	case <-time.After(10 * time.Second):
		return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
	}
}

問題点: Slow tests タイムアウト の test に 10秒 かかってしまう -> 引数で 秒数 の設定を可能に

  • What we can do is make the timeout configurable. (できることは timeout を構成可能にすることです)
  • Racer 関数 の 引数 に time.Duration 追加
func Racer(a, b string, timeout time.Duration) (winner string, error error) {
	select {
	case <-ping(a):
		return a, nil
	case <-ping(b):
		return b, nil
	case <-time.After(timeout):
		return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
	}
}

同じ コンパイルエラー が 2箇所で

not enough arguments in call to Racer
	have (string, string)
	want (string, string, time.Duration)
not enough arguments in call to Racer
	have (string, string)
	want (string, string, time.Duration)

呼び出し元の test code ( [1] 正常系の test と [2] timeout の test ) で 第3引数 を指定していないため

 

ConfigurableRacer

[1] 正常系の test (timeout しない) の場合:

v3 の racer_test.go

got, err := Racer(slowURL, fastURL)
  • 従来通り 2個の引数 で 呼び出し したい
  • 2個の引数 で 呼び出し した場合は デフォルト値 (10 * time.Second) を適用 したい
    • 正常系の test では 10秒かからずに 直ぐに終了するため デフォルト値 のままでも大丈夫
  • しかし go言語 には デフォルト引数 は 無い

[2] timeout の test の場合:

v3 の racer_test.go

_, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond)
  • Racer の timeout の test は時間がかかる(デフォルト10秒)ので、 ConfigurableRacer の 第3引数 に 20ms を渡して test
  • Racer(server.URL, server.URL, 20*time.Millisecond) という書き方はできない
    • go言語 は オーバーロード をサポートしていない ため
    • 引数が異なる場合は 別の関数名 で定義する必要有り
      • Racer(a, b string)ConfigurableRacer(a, b string, timeout time.Duration)

 

変更内容:

var tenSecondTimeout = 10 * time.Second
func Racer(a, b string) (winner string, error error) {
	return ConfigurableRacer(a, b, tenSecondTimeout)
}

func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, error error) {
	select {
	case <-ping(a):
		return a, nil
	case <-ping(b):
		return b, nil
	case <-time.After(timeout):
		return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
	}
}
  • 3個の引数の Racer 関数 -> ConfigurableRacer に 変更
  • func Racer(a, b string) (winner string, error error) {
    • 新たに 従来通り 2個の引数 の Racer 関数 を定義
      • ConfigurableRacer 関数 の 第3引数 に デフォルト値 を渡して実行

v3 終了

 

補足(別解): Functional Option Pattern で デフォルト引数 を実現

以下、 12. Select に記述はありませんが、補足として追記

  • Functional Option Pattern
    • go 言語で デフォルト引数, オプショナル引数 を 実現させるための パターンの1つ

Functional Option Pattern 参考サイト (1)

 

別解 (1): racer.go に追記

type RacerConfig struct {
	timeout time.Duration
}

type RacerOption func(*RacerConfig)

func WithTimeout(d time.Duration) RacerOption {
	return func(c *RacerConfig) {
		c.timeout = d
	}
}
  • RacerOption
    • 「 デフォルト値 を集めた struct(=RacerConfig) 」 の ポインタ型 を 引数 に取る 関数 なのがポイント

別解 (1): racer.go 修正

-var tenSecondTimeout = 10 * time.Second
+const defaultTimeout = 10 * time.Second
  • 変数名がイケてないので変更. (この変更は Functional Option Pattern とは無関係)
-func Racer(a, b string) (winner string, error error) {
-	return ConfigurableRacer(a, b, tenSecondTimeout)
-}
-
-func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, error error) {
  • 2個 引数 の Racer 関数は削除
  • ConfigurableRacer -> Racer に 関数名 を戻す ↓
  • 3個目 の 引数 の定義を 可変長引数(...) に変更
+func Racer(a, b string, options ...RacerOption) (winner string, error error) {
+	config := RacerConfig{timeout: defaultTimeout}
+
+	for _, option := range options {
+		option(&config)
+	}
	
	select {
	case <-ping(a):
		return a, nil
	case <-ping(b):
		return b, nil
-	case <-time.After(timeout):
+	case <-time.After(config.timeout):
		return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
	}
}

 

別解: racer_test.go [1] 正常系の test (timeout しない) の場合:

got, _ := Racer(slowURL, fastURL)  // 変更無し

別解: racer_test.go [2] timeout の test の場合:

-_, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond)
+_, err := Racer(server.URL, server.URL, WithTimeout(20*time.Millisecond))
  • ConfigurableRacer ではなく Racer の 第3引数 に 20ms を渡して test

 

別解 (1): racer.go 完成形

type RacerConfig struct {
	timeout time.Duration
}

type RacerOption func(*RacerConfig)

func WithTimeout(d time.Duration) RacerOption {
	return func(c *RacerConfig) {
		c.timeout = d
	}
}

const defaultTimeout = 10 * time.Second

func Racer(a, b string, options ...RacerOption) (winner string, error error) {
	config := RacerConfig{timeout: defaultTimeout}

	for _, option := range options {
		option(&config)
	}
	// 中略...
}

 

(以下、第6回(2020/02/05) 発表後に追記)

Functional Option Pattern 参考サイト (2)

別解 (2): racer.go 完成形

type RacerConfig struct {
	timeout time.Duration
}

type RacerOption interface {
	Apply(*RacerConfig)
}

type timeout time.Duration

func (t timeout) Apply(c *RacerConfig) {
	c.timeout = time.Duration(t)
}

func WithTimeout(d time.Duration) RacerOption {
	return timeout(d)
}

const defaultTimeout = 10 * time.Second

func Racer(a, b string, options ...RacerOption) (winner string, error error) {
	config := RacerConfig{timeout: defaultTimeout}

	for _, option := range options {
		option.Apply(&config)
	}
	// 以下、 別解 (1) と同じ
}
  • RacerOption
    • 「 デフォルト値 を集めた struct(=RacerConfig) 」 の ポインタ型 を 引数 に取る 関数 Apply を定義した interface なのがポイント
  • 別解 (1) の option(&config) を -> option.Apply(&config) に変更

 

別解 (1) と (2) の racer.go 差分

type RacerConfig struct {
	timeout time.Duration
}

-type RacerOption func(*RacerConfig)
+type RacerOption interface {
+	Apply(*RacerConfig)
+}

+type timeout time.Duration

+func (t timeout) Apply(c *RacerConfig) {
+	c.timeout = time.Duration(t)
+}

func WithTimeout(d time.Duration) RacerOption {
-	return func(c *RacerConfig) {
-		c.timeout = d
-	}
+	return timeout(d)
}

const defaultTimeout = 10 * time.Second

func Racer(a, b string, options ...RacerOption) (winner string, error error) {
	config := RacerConfig{timeout: defaultTimeout}

	for _, option := range options {
-		option(&config)
+		option.Apply(&config)
	}
	// 以下、 別解 (1) と同じ
}

 

Functional Option Pattern 参考サイト (3) grpc-goDialOption

-> DialOption (v1.27.1) の実装 参照

type DialOption interface {
	apply(*dialOptions)
}

別解 (3): racer.go 完成形

type RacerConfig struct {
	timeout time.Duration
}

type RacerOption interface {
	Apply(*RacerConfig)
}

type funcRacerOption struct {
	f func(*RacerConfig)
}

func (fro *funcRacerOption) Apply(c *RacerConfig) {
	fro.f(c)
}

func newFuncRacerOption(f func(*RacerConfig)) *funcRacerOption {
	return &funcRacerOption{
		f: f,
	}
}

func WithTimeout(d time.Duration) RacerOption {
	return newFuncRacerOption(func(c *RacerConfig) {
		c.timeout = d
	})
}

// 以下、 別解 (2) と同じ

別解 (2) と (3) の racer.go 差分

type RacerConfig struct {
	timeout time.Duration
}

type RacerOption interface {
	Apply(*RacerConfig)
}

+type funcRacerOption struct {
+	f func(*RacerConfig)
+}

+func (fro *funcRacerOption) Apply(c *RacerConfig) {
+	fro.f(c)
+}

+func newFuncRacerOption(f func(*RacerConfig)) *funcRacerOption {
+	return &funcRacerOption{
+		f: f,
+	}
+}

-type timeout time.Duration
-
-func (t timeout) Apply(c *RacerConfig) {
-	c.timeout = time.Duration(t)
-}
-
func WithTimeout(d time.Duration) RacerOption {
-	return timeout(d)
	return newFuncRacerOption(func(c *RacerConfig) {
		c.timeout = d
	})
}

// 以下、 別解 (2) と同じ

 

Functional Option Pattern 参考サイト (4)

  • Go API のための再利用可能で型安全なオプションの実装方法 - Frasco
    • > API(GET や PUT)ごとに違った引数の型を定義することにより解決したくなります
    • > 例えば、Get を受け付ける GetOption 、Put のみを受け付けられる PutOption を定義する、などです
    • > ここで注意すべきことは、関数が *Option インターフェースが埋め込まれた無名のインターフェースを返していることです

 

ソース構成

 


(番外編). 「Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る」 第8章(8.3 8.4) 解説

(番外編). 「Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る」 第8章(8.3 8.4) 解説

 


※ 今回はここまで

次回 -> 第7回(2020/02/19) 実施です (第7回(2020/02/19) 発表メモ)

 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.