Skip to content

Instantly share code, notes, and snippets.

@ohtsuchi
Last active April 9, 2023 22:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ohtsuchi/41b75a3d3f646f1d2d5600287ffe7f8c to your computer and use it in GitHub Desktop.
Save ohtsuchi/41b75a3d3f646f1d2d5600287ffe7f8c to your computer and use it in GitHub Desktop.
Go言語でTDDする勉強会 第3回(2019/12/18) 発表メモ

Go言語でTDDする勉強会 第3回(2019/12/18) 発表メモ

connpass

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

Slack

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

channel: #go

前提

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

教材

https://github.com/quii/learn-go-with-tests

参加者への質問

  • 初回(2019/11/20), 2回目(2019/12/04) 未参加で、 今回が初参加の方 はいますか?
    • ローカル環境でGo言語が動作する状態ですか?
    • Go言語を動かしたことはありますか?

本勉強会の管理者

今回の発表者

第5回 以降の発表者募集

  • 次回(第4回) は つっきー さんが担当するそうです
  • 別資料は作る必要なし
    • 前回(第2回) と 今回 は箇条書きレベルのメモを作成しましたが、特に別資料を作成しなくても大丈夫です
  • 興味ある方は slack で立候補してみて下さい
    • いなければ、自分がまた担当したいです

教材の説明

https://github.com/quii/learn-go-with-tests

  • Table of contentsGo fundamentals の欄
    • 1. Install Go から 17. Maths まで
  • さらに その下の Build an application の欄
    • HTTP server など、application を作成しながら TDD を学ぶ構成
  • 各章毎に、解説とソースコードが並ぶ構成
  • 全ての章の ページ先頭 に、ソースファイルが格納されているディレクトリへの リンク(さらにその直下に v1, v2, etc...)
    • 基本的に解説内にソースコードも記述されていますが、たまに情報不足の時があるのでその時はファイルを確認して下さい

例: 2. Hello, worldFrench

    if language == french {
        return frenchHelloPrefix + name
    }

定数: french, frenchHelloPrefix の定義部分が載っていない

v6 のソースファイル で確認

// 前略
const french = "French"
// 中略
const frenchHelloPrefix = "Bonjour, "
// 以下略

ディレクトリ構成

2. Hello, World

  • it's up to you how you structure your folders. (フォルダをどのように構成するかはあなた次第です)

前回 の 発表メモ

第2回(2019/12/04) 発表メモ

  • 前回: 5. Arrays and slices の 2回目のRefactor(coverage の確認)まで終了
  • 今回: 5. Arrays and slices の 3回目のWrite the test first(SumAll 関数の test 作成) から

5. Arrays and slices

5. Arrays and slices

  • Write the test first
    • sum_test.go に 新規 test (TestSumAll) 追加
      • got := SumAll([]int{1,2}, []int{0,9})
  • Try and run the test
    • go test
      • undefined: SumAll
      • SumAll 関数 未定義なので コンパイルエラー
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • SumAll 関数 を定義
      • func SumAll(numbersToSum ...[]int) (sums []int) {
    • Variadic Function (可変長引数の関数)
    • コンパイル失敗
      • sum_test.goif got != want { の行
      • invalid operation: got != want (slice can only be compared to nil)
        • Go does not let you use equality operators with slices. (Goでは、スライスで等値演算子を使用できません)
        • 補足: Go maps in action - The Go Blog 参照
          • > comparable types are boolean, numeric, string, pointer, channel, and interface types, and structs or arrays that contain only those types. (比較可能な型は、ブール型、数値型、文字列型、ポインター型、チャネル型、インターフェイス型、およびこれらの型のみを含む構造体または配列です)
          • > Notably absent from the list are slices, maps, and functions; these types cannot be compared using ==, and may not be used as map keys. (特にリストにないのは、slice, map, 関数 です。 これらの 型 は==を使用して比較することはできず, マップキーとして使用することはできません。)
    • reflect.DeepEqual 使用
      • useful for seeing if any two variables are the same. (2つの変数が同じであるかどうかを確認するのに役立ちます)
      • not "type safe"
        • slicestring との比較 -> コンパイル できてしまう
      • 補足: DeepEqual の定義:
        • func DeepEqual(x, y interface{}) bool
          • 補足: A Tour of Go: The empty interface
            • > ゼロ個のメソッドを指定されたインターフェース型は、 空のインターフェース と呼ばれます
            • > 空のインターフェースは、任意の型の値を保持できます
    • go test
      • got [] want [3 9]
      • 期待値が異なるので テスト失敗
  • Write enough code to make it pass
    • SumAll 関数 の中身を実装
      • 戻り値 (sums []int) (Named return value で 戻り値の変数名 sums を定義) も []int に変更
        • (sums []int) のままでも問題無いが、1つ後の Refactor の変更(var sums []int) で、 コンパイルエラー が出るため
        • sums := make([]int, lengthOfNumbers) の行で、 コンパイルエラー が出るため
          • エラー内容: no new variables on left side of :=
    • len
      • len(numbersToSum)
      • slice の長さ を取得
    • make
    • go test -> test pass
  • v4 終了
  • Refactor
    • slices have a capacity (slice は capacity を持ちます)
      • If you have a slice with a capacity of 2 and try to do mySlice[10] = 1 you will get a runtime error. (capacity が 2 の slice がで mySlice [10] = 1 を実行しようとすると runtime error が発生します)
    • SumAll 関数 の中身を変更
    • append
      • sums = append(sums, Sum(numbers))
      • In this implementation, we are worrying less about capacity (この実装では capacity について心配する必要はありません)
      • 補足: A Tour of Go: Appending to a slice 参照
        • printSlice 関数内の %v -> %#v に変更して、実行してみて下さい
          • 一番最初の時点の s (var s []int) は nil である 事が分かる
        • main 関数内の printSlice(s) 呼び出しの後に fmt.Printf("%p\n", s) を追加してみて下さい
          • capacity が増えたタイミングで slice の アドレスが変わる 事が分かる
      • 補足: 50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang DevsUsing "nil" Slices and Maps 参照
        • > It's OK to add items to a "nil" slice, but doing the same with a map will produce a runtime panic. ("nil" slice にアイテムを追加してもかまいませんが、 map で同じ操作を行うと runtime panic が発生します)
    • go test -> test pass
    • next requirement -> SumAllSumAllTails に変更
      • Tail = 各 slice の先頭1件目を除いた、2件目以降
  • v5 終了
  • Write the test first
    • sum_test.go の tet TestSumAllTestSumAllTails に変更
      • test 関数名: TestSumAll -> TestSumAllTails
      • test の中で呼び出す 関数名: SumAll -> SumAllTails
      • 期待値: want := []int{3, 9} -> []int{2, 9}
  • Try and run the test
    • go test
      • undefined: SumAllTails
      • SumAllTails 関数 未定義なので コンパイルエラー
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • Rename the function to SumAllTails
    • go test
      • got [3 9] want [2 9]
      • 期待値が異なるので テスト失敗
        • TestSumAll のロジックから変更していないので、全ての値の合計値が返る
  • Write enough code to make it pass
    • for の中身を変更
      • 変更前
        • sums = append(sums, Sum(numbers))
      • 変更後
        • tail := numbers[1:]
        • sums = append(sums, Sum(tail))
    • slice[low:high]
  • v6 終了
  • Refactor
    • What do you think would happen if you passed in an empty slice into our function? (空のスライスを関数に渡すとどうなると思いますか?)
  • Write the test first
    • TestSumAllTails 関数 の中身を t.Run で囲む
      • t.Run("make the sums of some slices", ...
    • test 追加 (t.Run("safely sum empty slices", ...)
      • got := SumAllTails([]int{}, []int{3, 4, 5})
        • []int{}SumAllTails の引数に渡す
  • Try and run the test
    • go test
      • panic: runtime error: slice bounds out of range 発生
        • tail := numbers[1:] の部分
      • It's important to note the test has compiled, it is a runtime error. (test がコンパイルされたことに注意することが重要です. これは runtime error です)
  • Write enough code to make it pass
    • SumAllTails 関数の実装変更
      • for の中身を変更
      • if else 追加
        • if len(numbers) == 0 {
    • go test -> test pass
  • Refactor
    • repeated code -> extract that into a function
      • checkSums ヘルパー関数 を追加
      • A handy side-effect of this is this adds a little type-safety to our code (これの便利な副作用は、コードに少し 型安全性を追加 することです)
        • もし checkSums(t, got, "dave") と記述した場合は コンパイルエラー
          • cannot use "dave" (type string) as type []int in argument to checkSums
  • v7 終了
    • 注: t.Helper() が抜けている? ので注意
  • Wrapping up

 


以下 5. Arrays and slices から リンクされていた記事の紹介

Go Slices: usage and internals - The Go Blog

Go Slices: usage and internals - The Go Blog

Slice internals

make([]byte, 5)

s = s[2:4]

s = s[:cap(s)]
補足: cap(s) はこの場合 3 を返すので s = s[:3] と同じ.

A slice cannot be grown beyond its capacity. Attempting to do so will cause a runtime panic, (slice は capacity を超えて成長させることはできません. そうしようとすると runtime panic が発生します)
補足: capacity (参照先の配列への pointer から数えた残りサイズ) を超える 4 を指定すると(s = s[:4]) panic 発生

補足: capacity を超える拡張をしたければ append 関数を利用する
-> How to avoid Go gotchas · divan's blogAppend 参照

Similarly, slices cannot be re-sliced below zero to access earlier elements in the array. (同様に slice をゼロより下に再スライスして 配列内の以前の要素にアクセスすることはできません)
補足: pointer が 参照先の配列の3番目を指しているので, 1番目と2番目の要素にはもう s からアクセスできない

 

Here is an example of slicing an array

https://play.golang.org/p/bTrRmYfNYCp

a "copy" of the slice will not affect the original array (slice の "copy" は元の array に影響しません)

補足: Common Gotchas in Go - 0xDEADBEEF 参照 の 3.) Slicing

> slicing in Python gives you a new list (Python では スライスすると新しい list が得られます)
> However if you try the same thing in Go, (略) (しかし、 Go で同じことを試してみると、)
> In Go, a slice shares the same backing array and capacity as the original (Go では slice は、元と同じ backing array と capacity を共有します)

解決策 ↓

// Option #1
// appending elements to a nil slice
// `...` changes slice to arguments for the variadic function `append`
a := append([]int{}, data[:2]...)

// Option #1
// Create slice with length of 2
// copy(dest, src)
a := make([]int, 2)
copy(a, data[:2])

 

https://play.golang.org/p/Poth8JS28sc

why it's a good idea to make a copy of a slice after slicing a very large slice. (非常に大きな slice をスライスした後に slice の copy を作成する事をお勧めする理由)

package main

import (
	"fmt"
)

func main() {
	a := make([]int, 1e6) // slice "a" with len = 1 million
	b := a[:2] // even though "b" len = 2, it points to the same the underlying array "a" points to
	
	c := make([]int, len(b)) // create a copy of the slice so "a" can be garbage collected
	copy(c, b)
	fmt.Println(c)
	
	// 追記
	printSlice(a)
	printSlice(b)
	printSlice(c)
}

// A Tour of Go ( https://go-tour-jp.appspot.com/moretypes/15 ) からコピー
func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

実行結果

[0 0]
len=1000000 cap=1000000 [0 0 0 0 0 0, ...]
len=2 cap=1000000 [0 0]
len=2 cap=2 [0 0]

補足: 100万個の slice(=a) のうち、 先頭の2個だけ(=b)を必要 になり、
途中から a が 不要 になったとしても、
b を使用している限りb と同じ配列を指す a も生き続ける

ab とは 別の配列を指す c にデータを copy して、
以後は c だけ使用 して、 且つ ab が スコープ外になれば、
ab が指す 100万個 は garbage の対象 となる. という意味のはず...

 


6. Structs, methods & interfaces

6. Structs, methods & interfaces

  • Structs, methods & interfaces
    • Perimeter (周囲の長さ, 外周) を計算する 関数 を作ります
      • calculate the perimeter of a rectangle given a height and width. (与えられた高さと幅の長方形の周囲を計算します)
  • Write the test first
    • structs ディレクトリ作成
      • ディレクトリ移動 cd ../structs
    • shapes_test.go 作成
      • package main
      • TestPerimeter 関数作成
    • %.2f
      • 小数点以下2桁を出力
  • Try to run the test
    • go test
      • undefined: Perimeter
      • Perimeter 未定義なので コンパイルエラー
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • shapes.go 作成
    • Perimeter 関数作成
      • return 0
    • go test
      • got 0.00 want 40.00.
      • 期待値が異なるので テスト失敗
  • Write enough code to make it pass
    • go test -> test pass
    • v1 終了
    • 次に Area 関数の test (TestArea) 追加
    • Area 関数追加
    • go test -> test pass
    • v2 終了
  • Refactor
    • it doesn't contain anything explicit about rectangles (長方形について明示的なものは何も含まれていません)
    • An unwary developer might try to supply the width and height of a triangle (不注意な開発者は、三角形の幅と高さを指定しようとするかもしれません)
    • 解決策は以下 のいずれか
      • (1) give the functions more specific names like RectangleArea (関数に RectangleArea などの、より具体的な名前を付ける)
      • (2) A neater solution is to define our own type called Rectangle (よりきれいな解決策は、 Rectangle と呼ばれる 独自の型を定義 する)
    • Declare a struct
      • type Rectangle struct {...}
        • shapes.go の先頭に追加
    • test 変更. 関数に渡す引数を変更
      • got := Perimeter(10.0, 10.0) -> got := Perimeter(rectangle)
      • got := Area(12.0, 6.0) -> got := Area(rectangle)
    • go test
      • not enough arguments in call to Perimeter
        • have (Rectangle)
        • want (float64, float64)
      • 引数の (width float64, height float64) と合わないので コンパイルエラー
    • Perimeter 関数, Area 関数 を変更
      • 引数を (width float64, height float64) -> (rectangle Rectangle) に変更
    • myStruct.field の syntax を使用して field にアクセス
    • go test -> test pass
    • passing a Rectangle to a function conveys our intent more clearly (関数に Rectangle を渡すことで、意図がより明確に 伝わります)
    • next requirement -> Area function for circles
  • v3 終了
  • Write the test first
    • TestArea 関数 の中身を t.Run で囲む
      • t.Run("rectangles", ...
    • TestArea 関数 に test (t.Run("circles", ...) 追加
    • %g
      • -> fmt - The Go Programming Language
        • > %g %e for large exponents, %f otherwise
          • > %e scientific notation, e.g. -1.234456e+78
          • > %f decimal point but no exponent, e.g. 123.456
  • Try to run the test
    • go test
      • undefined: Circle
      • Circle 未定義なので コンパイルエラー
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • Circle 型 を定義
      • shapes.go の末尾に追加
    • Circle を引数に取る Area 関数を定義
func Area(c Circle) float64 {
	return math.Pi * c.Radius * c.Radius
}
  • Write the minimal amount of code ... (続き)
    • go test
      • Area redeclared in this block コンパイルエラー
      • Go では オーバーロード できないため
    • 次の2つの選択肢 のいずれか
      • (1) 別の package で定義 -> overkill (やりすぎ)
      • (2) method を定義
  • What are methods?
    • test 変更. メソッド 呼び出しに変更
      • got := Area(rectangle) -> got := rectangle.Area()
      • got := Area(circle) -> got := circle.Area()
    • go test
      • rectangle.Area undefined
      • circle.Area undefined
      • メソッド 未定義なので コンパイルエラー
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • Rectangle 型 と Circle 型 に Area メソッド 追加
      • func (r Rectangle) Area() float64
      • func (c Circle) Area() float64
      • return 0
      • 前に実装した Area 関数は削除
    • func (receiverName RecieverType) MethodName(args)
      • The syntax for declaring methods is almost the same as functions (method を宣言するための syntax は function とほぼ同じ)
        • The only difference is the syntax of the method receiver (唯一の違いmethod receiver の syntax です)
      • receiverName 変数 -> 他の言語だと thisself
      • It is a convention in Go to have the receiver variable be the first letter of the type (Goでは、 receiver 変数 を 型の最初の文字 にすることが慣例となっています。)
        • r Rectangle
    • 補足: A Tour of Go: Methods 参照
    • go test
      • got 0.00 want 72.00
      • got 0 want 314.1592653589793
      • 期待値が異なるので テスト失敗
  • Write enough code to make it pass
    • RectangleCircleArea メソッド 正しい実装に変更
    • go test -> test pass
  • v4 終了
  • Refactor
	t.Run("rectangles", func(t *testing.T) {
		rectangle := Rectangle{12, 6}
		got := rectangle.Area()
		want := 72.0

		if got != want {
			t.Errorf("got %g want %g", got, want)
		}
	})

Area() の呼び出し部と、 assertion の部分を ヘルパー関数 として抽出

	checkArea := func(t *testing.T, rectangle Rectangle, want float64) {
		t.Helper()
		got := rectangle.Area()
		if got != want {
			t.Errorf("got %g want %g", got, want)
		}
	}

	t.Run("rectangles", func(t *testing.T) {
		rectangle := Rectangle{12, 6}
		checkArea(t, rectangle, 72.0)
	})

CirclecheckArea の 第2引数 に渡すと コンパイルエラー
go test -> cannot use circle (type Circle) as type Rectangle in argument to checkArea
引数の型 が Rectangle に対して Circle を渡しているため

	t.Run("circles", func(t *testing.T) {
		circle := Circle{10}
		checkArea(t, circle, 314.1592653589793)
	})

↓ 引数の型を Shape に変更

-	checkArea := func(t *testing.T, rectangle Rectangle, want float64) {
+	checkArea := func(t *testing.T, shape Shape, want float64) {
		t.Helper()
-		got := rectangle.Area()
+		got := shape.Area()

go test -> undefined: Shape
Shape 未定義のため コンパイルエラー

interface Shape を定義 (shapes.go の先頭に追加)

type Shape interface {
	Area() float64
}

go test -> test pass

  • v5 終了
  • Wait, what?
    • This is quite different to interfaces in most other programming languages. (これは、他のほとんどのプログラミング言語の interface とはまったく異なります)
    • Normally you have to write code to say My type Foo implements interface Bar. (通常 Foo implements Bar というコードを書かなければなりません)
    • In Go interface resolution is implicit. (Go では interface の解決は暗黙的 です)
      • 補足: A Tour of Go: Interfaces are implemented implicitly 参照
        • > 型にメソッドを実装していくことによって、インタフェースを実装(満た)します
        • > インタフェースを実装することを明示的に宣言する必要はありません
          • > "implements" キーワードは必要ありません
  • Decoupling
  • Further refactoring
    • TestArea 関数 の中身を全て変更
    • Table driven tests
      • declaring a slice of structs (struct の slice を宣言)
        • anonymous struct
      • very easy to add a new test case (非常に簡単に新しい test case を追加できます)
      • If you wish to test various implementations of an interface, ... then they are a great fit. (interface のさまざまな実装を test する場合、最適です)
    • adding another shape and testing it; a triangle. (別の形状を追加してテストします。 三角形)
  • v6 終了
  • Write the test first
    • {Triangle{12, 6}, 36.0}, 追加
  • Try to run the test
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • go test
      • undefined: Triangle
      • Triangle 未定義なので コンパイルエラー
    • Triangle 定義
      • shapes.go の末尾に追加
    • go test
      • cannot use Triangle literal (type Triangle) as type Shape in field value:
        • Triangle does not implement Shape (missing Area method)
      • Triangle 型 が Area メソッド を 実装 していないので コンパイルエラー
    • Triangle 型 に Area メソッド を実装
      • return 0
    • go test
      • got 0.00 want 36.00
      • 期待値が異なるので テスト失敗
  • Write enough code to make it pass
    • Triangle 型の Area メソッド 正しい実装に変更
    • go test -> test pass
  • v7 終了
  • Refactor
		{Rectangle{12, 6}, 72.0},
		{Circle{10}, 314.1592653589793},
		{Triangle{12, 6}, 36.0},

↓ struct 作成時 の syntax: MyStruct{val1, val2} に optionally で name を付ける事ができる

		{shape: Rectangle{Width: 12, Height: 6}, want: 72.0},
		{shape: Circle{Radius: 10}, want: 314.1592653589793},
		{shape: Triangle{Base: 12, Height: 6}, want: 36.0},
  • Make sure your test output is helpful
    • t.Errorf の format 文字列に %#v 追加
      • %#v
        • print out our struct with the values in its field (field の値付きで struct を出力します)
        • 補足: fmt - The Go Programming Language
          • > %#v a Go-syntax representation of the value
      • shape の内容を表示
    • t.Errorf の 第2引数 に tt.shape を渡す
    • struct の field 名: want -> hasArea に変更
    • struct の field 追加: name
      • t.Run の 第1引数 に渡す
    • t.Run
      • test case に 名前を付ける
    • go test -run XXX
      • 特定のテストを実行
  • v8 終了

 


7. Pointers & errors

7. Pointers & errors

  • Write the test first
    • pointers ディレクトリ作成
      • ディレクトリ移動 cd ../pointers
    • wallet_test.go 作成
      • package main
    • 内部状態 を expose したくない -> method を通してアクセス(Balance())
  • Try to run the test
    • go test
      • undefined: Wallet
      • Wallet を定義していないので コンパイルエラー
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • wallet.go 作成
    • Wallet 型を定義
    • go test
      • wallet.Deposit undefined (type Wallet has no field or method Deposit)
      • wallet.Balance undefined (type Wallet has no field or method Balance)
      • メソッド (Deposit, Balance) を定義していないので コンパイルエラー
    • メソッド (Deposit, Balance) を定義
    • go test
      • got 0 want 10
      • 期待値が異なるので テスト失敗
  • Write enough code to make it pass
    • Wallet 型に int 型の変数 balance 追加
      • 小文字始まり
      • package 単位で private
        • our methods to be able to manipulate this value but no one else. (メソッドがこの値を操作できるようにしたいが、他の誰も操作できないようにします)
    • メソッド (Deposit, Balance) 正しい実装に変更
      • we can access the internal balance field in the struct using the "receiver" variable ("receiver" 変数を使用して struct 内の内部 balance フィールドにアクセスできます。)
        • w.balance += amount
        • return w.balance
    • go test
      • got 0 want 10
      • 期待値が異なるので テスト失敗
  • ????
    • In Go, when you call a function or a method the arguments are copied. (Goでは、関数またはメソッドを呼び出すと、引数が copy されます)
      • func (w Wallet) Deposit(amount int) を呼び出す時、 w が copy されている
    • 変数の address (&wallet, &w) を出力して実験
    • We can fix this with pointers
      • (w Wallet) -> (w *Wallet) に変更
    • 補足: A Tour of Go: Choosing a value or pointer receiver 参照
      • > ポインタレシーバを使う2つの理由があります
        • > ひとつは、メソッドがレシーバが指す先の変数を変更するためです
        • > ふたつに、メソッドの呼び出し毎に変数のコピーを避けるためです。 例えば、レシーバが大きな構造体である場合に効率的です
      • > 例では、 Abs メソッドはレシーバ自身を変更する必要はありませんが、 Scale と Abs は両方とも *Vertex 型のレシーバです
        • > 一般的には、変数レシーバ、または、ポインタレシーバのどちらかですべてのメソッドを与え、混在させるべきではありません
    • 補足: 書籍 プログラミング言語Go の「 第6章 メソッド 」の「 6.2 ポインタレシーバを持つメソッド 」 (p180)
      • The Go Programming Language - Alan A. A. Donovan, Brian W. Kernighan - Google ブックス 参照
        • > convention dictates that if any method of Point has a pointer receiver, then all methods of Point should have a pointer receiver, even ones that don't strictly need it. (慣例では Pointの メソッド に pointer receiver がある場合、厳密には必要なくても Point の全てのメソッドは pointer receiver を持つべき です)
func (w *Wallet) Deposit(amount int) {
	w.balance += amount
}

func (w *Wallet) Balance() int {
	return w.balance
}
  • ???? (続き)
    • go test -> test pass
    • w.balance
      • automatically dereferenced
        • pointer(w) を 明示的に dereference ((*w).balance) しなくても、自動でしてくれる
func (w *Wallet) Balance() int {
	return (*w).balance
}
  • Refactor
    • Go lets you create new types from existing ones. (Goでは、既存の type から新しい type を作成できます)
      • type MyName OriginalType
    • type Bitcoin int 定義
      • wallet.go の先頭に追加
    • balance int -> balance Bitcoin に変更
      • メソッド の 引数 や 戻り値 の 型 を int -> Bitcoin に変更
      • test code の 10 -> Bitcoin(10) に変更
    • Bitcoin 型 に Stringer interface を実装
      • Bitcoin 型 に String() メソッド追加
        • define how your type is printed when used with the %s format string (%s format 文字列と共に使用した場合に 型 がどのように print されるかを定義します)
      • test の t.Errorf("got %d want %d", got, want) -> t.Errorf("got %s want %s", got, want) に変更(%d -> %s)
        • String() メソッドの戻り値が使用される
          • deliberately break the test so we can see it (意図的に test を壊して それを確認)
          • go test
            • got 10 BTC want 20 BTC
      • 補足: A Tour of Go: Methods continued
        • > 任意の型(type)にもメソッドを宣言できます
        • > レシーバを伴うメソッドの宣言は、レシーバ型が同じパッケージにある必要があります
          • > 他のパッケージに定義している型に対して、レシーバを伴うメソッドを宣言できません
          • > (組み込みの int などの型も同様です)
      • 補足: A Tour of Go: Stringers 参照
        • > Stringer インタフェースは、stringとして表現することができる型です
    • next requirement -> Withdraw
  • v1 終了
  • Write the test first
    • TestWallet 関数 の中身を t.Run で囲む
      • t.Run("Deposit", ...
    • t.Run("Withdraw", ... 追加
  • Try to run the test
    • go test
      • wallet.Withdraw undefined (type Wallet has no field or method Withdraw)
      • Wallet 型 に Withdraw メソッド 未定義なので コンパイルエラー
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • Wallet 型 の Withdraw メソッド 追加
      • 空実装
    • go test
      • got 20 BTC want 10 BTC
      • 期待値が異なるので テスト失敗
  • Write enough code to make it pass
    • Wallet 型 の Withdraw メソッド 正しい実装に変更
    • go test -> test pass
  • Refactor
    • 重複部分 -> Helper 関数 (assertBalance) 抽出
    • go test -> test pass
    • 残高以上を引き出そうとしたら?
      • -> error を返す
  • v2 終了
  • Write the test first
    • TestWallet 関数 内に t.Run("Withdraw insufficient funds", ... 追加
    • 残高よりも多く引き出そうとした時に Withdrawerror を返す 事を確認
      • 戻り値の errornil になる可能性
        • errorinterface のため
      • errornil かどうかチェック する
        • if err == nil
          • error が返ってきたら test 成功にしたいので、 nil ならば test 失敗
  • Try and run the test
    • go test
      • wallet.Withdraw(Bitcoin(100)) used as value
      • 注意: エラーメッセージが分かりづらい...
        • err := wallet.Withdraw(Bitcoin(100)) の行が原因
        • 変数 err に代入 しているのに Withdraw は 戻り値 をまだ定義していない ため
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • Wallet 型 の Withdraw メソッド 変更
      • 戻り値の型 error 追加
      • return nil 1行追加
    • go test
      • got -80 BTC want 20 BTC
        • 期待値が異なるので テスト失敗
      • wanted an error but didn't get one
        • if err == nil の チェック で テスト失敗
  • Write enough code to make it pass
    • Wallet 型 の Withdraw メソッド 変更
      • 残高チェック追加 (if amount > w.balance)
        • errors.New で作成 した error を返す
          • creates a new error with a message of your choosing. (選択したメッセージを使用して、新しい error を作成します)
  • Refactor
    • Helper 関数 (assertError) 抽出
  • v3 終了
  • Write the test first
    • assertError 関数
      • 第2引数 引数名 変更 err -> got
      • 第3引数 (want string) 追加
      • if got == nil 内の t.Error -> t.Fatal に変更
        • 中のエラーメッセージも変更
          • t.Fatal("didn't get an error but wanted one")
      • error文字列 (Error()) の assert 追加
        • 補足: イケてないので 後の Refactor で修正 されます
    • t.Fatal
      • stop the test if it is called (呼び出された場合にテストを停止します)
      • 次の if got.Error() != want { の行で panic になるのを防ぐため
        • got が nil
  • Try to run the test
    • go test
      • got err 'oh no' want 'cannot withdraw, insufficient funds'
      • 期待値が異なるので テスト失敗
  • Write enough code to make it pass
    • Wallet 型 の Withdraw 変更
      • if amount > w.balance の中
        • return errors.New("oh no") -> return errors.New("cannot withdraw, insufficient funds") に変更
  • go test -> test pass
  • Refactor
    • We have duplication of the error message in both the test code and the Withdraw code. (test code と Withdraw の両方で エラーメッセージが重複 しています)
    • var ErrInsufficientFunds = errors.New("cannot withdraw, insufficient funds") を定義
      • In Go, errors are values, so we can refactor it out into a variable (Go では、 error は値 であるため、変数にリファクタリングできます)
      • The var keyword allows us to define values global to the package (var キーワードは、私たちは package に対して グローバルな値を定義することができます)
        • 補足: ちなみに varconst に変更すると コンパイルエラー
          • 補足: A Tour of Go: Constants 参照
            • > 定数は、文字(character)、文字列(string)、boolean、数値(numeric)のみで使えます
          • 次章 8. Mapsstring を元type を定義した後に error を実装 して const する手順を学びます
    • Next we can refactor our test code to use this value instead of specific strings. (次に、特定の string の代わりに、この値を使用 するように test code をリファクタリング)
      • assertError の 第3引数を want string -> want error に変更
      • assertError(t, err, "cannot withdraw, insufficient funds") -> assertError(t, err, ErrInsufficientFunds) に変更
    • Helper 関数 (assertBalance, assertError) を main test 関数 (TestWallet) の外に移動
  • Unchecked errors
    • There is one scenario we have not tested. To find it, ... install errcheck (test していないシナリオが1つあります. それを見つけるには、... errcheck を install します)
    • go get -u github.com/kisielk/errcheck を terminal で実行
    • errcheck . を実行
      • wallet_test.go:17:18: wallet.Withdraw(Bitcoin(10))
        • What this is telling us is that we have not checked the error being returned on that line of code. (これは、コードのその行で返される error をチェックしていないことを示しています)
    • 戻り値の error チェックを追加 -> Helper 関数 (assertNoError) 追加
	t.Run("Withdraw with funds", func(t *testing.T) {
		wallet := Wallet{Bitcoin(20)}
-		wallet.Withdraw(Bitcoin(10))
+		err := wallet.Withdraw(Bitcoin(10))
+
		assertBalance(t, wallet, Bitcoin(10))
+		assertNoError(t, err)
	})
  • v4 終了
  • Wrapping up

 


以下 7. Pointers & errors から リンクされていた記事の紹介

Don't just check errors, handle them gracefully | Dave Cheney

Don't just check errors, handle them gracefully | Dave Cheney

  • 補足1: このサイトで解説している error handling は
    • 「途中で処理を止めて、エラー内容を上位層に伝播させないといけないエラー」
    • (他の言語で言うところの 例外の throw) の事を指していると思われます...
  • 補足2: 以下の3つの error を順々に説明し、 結論 としては 【(3) で且つ github.com/pkg/errors を利用】 することを suggest しているようです
    • (1) Sentinel errors
    • (2) Error types
    • (3) Opaque errors
  • 補足3: しかし Go 1.13 から 標準パッケージ にも github.com/pkg/errors と同様の機能が追加されたので、 今後はそちらの方を利用した方が良いのかも?

Errors are just values

  • Go's error handling can be classified into the three core strategies. (Go の error handling は、 3つのコア戦略 に分類できます)

(1) Sentinel errors

if err == ErrSomething { … }
  • 例: io.EOF
  • the caller must compare the result to predeclared value using the equality operator. (呼び出し側等値演算子 を使用 して 結果を事前宣言された値と比較 する必要がある)

Never inspect the output of error.Error (error.Error の出力を検査しないでください)

  • As an aside, I believe you should never inspect the output of the error.Error method. (余談 ですが、 error.Error メソッドの出力を検査しないで ください)
  • The Error method on the error interface exists for humans, not code. (error interface の Error メソッドは、コードではなく人間向けに存在します)

補足: いきなり余談で話がそれますが、 7. Pointers & errorsassertErrorError() の エラーメッセージ の assert をしていた

assertError := func(t *testing.T, got error, want string) {
	// 中略
	if got.Error() != want {
		t.Errorf("got %q, want %q", got, want)
	}
}

のを止めて ↓

func assertError(t *testing.T, got error, want error) {
	// 中略
	if got != want {
		t.Errorf("got %q, want %q", got, want)
	}
}

error 型の値の assert に変更 したのと同じ事が書かれています

Sentinel errors become part of your public API (Sentinel error は public API の一部になります)

  • We see this with io.Reader. Functions like io.Copy require a reader implementation to return exactly io.EOF to signal to the caller no more data, but that isn't an error. (これは io.Reader で確認できます. io.Copy のような関数は、呼び出し側にこれ以上データがないことを知らせるために、正確に io.EOF を返す reader 実装を必要としますが、それは error ではありません)

補足: io.Copy の実装 - go/io.go at release-branch.go1.13 · golang/go 参照

func Copy(dst Writer, src Reader) (written int64, err error) {
	return copyBuffer(dst, src, nil)
}

// (中略)

func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
	// (中略)
	for {
		// (中略)
		nr, er := src.Read(buf)
		if nr > 0 {
			// (中略)
		}
		if er != nil {
			if er != EOF {
				err = er
			}
			break
		}
	}
	return written, err
}
  • 補足: A Tour of Go: Readers 参照
    • > io.Reader インタフェースは Read メソッドを持ちます
    • > Read は、データを与えられたバイトスライスへ入れ、入れたバイトのサイズとエラーの値を返します
      • > ストリームの終端は、 io.EOF のエラーで返します
func main() {
	r := strings.NewReader("Hello, Reader!")

	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}

Sentinel errors create a dependency between two packages (Sentinel error は 2つの package 間の依存関係を作成します)

  • By far the worst problem with sentinel error values is they create a source code dependency between two packages. (sentinel error 値で 最もひどい問題は、2つの package 間にソースコードの依存関係を作成 することです)
  • As an example, to check if an error is equal to io.EOF, your code must import the io package. (例として、 error が io.EOF と等しいかどうかを確認するには、コードで io package を import しなければなりません)

Conclusion: avoid sentinel errors (結論: sentinel error を避ける)

(2) Error types

type MyError struct {
	Msg string
	File string
	Line int
}

func (e *MyError) Error() string { 
	return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Msg)
}
  • callers can use type assertion to extract the extra context from the error (呼び出し元は 型アサーション を使用して、 error から余分なコンテキストを抽出できます)
err := something()
switch err := err.(type) {
case nil:
	// call succeeded, nothing to do
case *MyError:
	fmt.Println("error occurred on line:", err.Line)
default:
// unknown error
}
  • A big improvement of error types over error values is their ability to wrap an underlying error to provide more context. (error 値 に対する Error types の大きな改善点は、基になる error をラップしてより多くの context を提供できることです)

Problems with error types (Error types の問題)

  • creates a strong coupling with the caller, making for a brittle API. (呼び出し元との強い結合を作成 し、脆弱な API を作成します)

Conclusion: avoid error types (Error types を避ける)

(3) Opaque errors (不透明な error)

  • the most flexible error handling strategy (最も柔軟な error handling 戦略)
  • just return the error without assuming anything about its contents. (内容について何も仮定せずにエラーを返すだけ)
import "github.com/quux/bar"

func fn() error {
	x, err := bar.Foo()
	if err != nil {
		return err
	}
	// use x
}

Assert errors for behaviour, not type (型 ではなく 動作 の error を assert する) ※ 後述

一旦飛ばして後述します.

Don't just check errors, handle them gracefully (error をチェックするだけでなく、適切に処理する)

func AuthenticateRequest(r *Request) error {
	err := authenticate(r.User)
	if err != nil {
		return err
	}
	return nil
}
  • the problem with this code is I cannot tell where the original error came from. (このコードの問題は、 元のエラーの原因を特定できない ことです。)

will be printed is

No such file or directory.

  • There is no information of file and line where the error was generated. (error が生成されたファイルと行の情報ありません)
  • There is no stack trace of the call stack leading up to the error. (error につながる呼び出し stack の stack traceありません)

The Go Programming Language (日本語版: プログラミング言語Go) では以下を推奨

func AuthenticateRequest(r *Request) error {
	err := authenticate(r.User)
	if err != nil {
		return fmt.Errorf("authenticate failed: %v", err)
	}
	return nil
}

しかしこのサイトでは推奨していない。理由は以下

  • converting the error value to a string, merging it with another string, then converting it back to an error with fmt.Errorf breaks equality and destroys any context in the original error. (error 値 を string に変換し、別の string とマージしてから fmt.Errorf を使用して error に戻す と、平等性が失われ、 元の error のコンテキストが破壊されます)

Annotating errors (error に注釈を付ける)

  • I’d like to suggest a method to add context to errors (error に context を追加 する方法を提案したい)

github.com/pkg/errors

// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
// Cause unwraps an annotated error.
func Cause(err error) error

-> The Go Playground

package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"

	"github.com/pkg/errors"
)

func ReadFile(path string) ([]byte, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, errors.Wrap(err, "open failed")
	}
	defer f.Close()

	buf, err := ioutil.ReadAll(f)
	if err != nil {
		return nil, errors.Wrap(err, "read failed")
	}
	return buf, nil
}

func ReadConfig() ([]byte, error) {
	home := os.Getenv("HOME")
	config, err := ReadFile(filepath.Join(home, ".settings.xml"))
	return config, errors.Wrap(err, "could not read config")
}

func main() {
	_, err := ReadConfig()
	if err != nil {
		fmt.Println(err)
		// errors.Print(err) // コンパイルエラー(undefined: errors.Print)

		fmt.Println("")
		fmt.Printf("%+v", err)
		os.Exit(1)
	}
}

実行結果

could not read config: open failed: open .settings.xml: No such file or directory

open .settings.xml: No such file or directory
open failed
main.ReadFile
	/tmp/sandbox650251579/prog.go:15
main.ReadConfig
	/tmp/sandbox650251579/prog.go:28
main.main
	/tmp/sandbox650251579/prog.go:33
(以下略)

 

Assert errors for behaviour, not type (型 ではなく 動作 の error を assert する) ※ 再掲

type temporary interface {
	Temporary() bool
}
 
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
	te, ok := err.(temporary)
	return ok && te.Temporary()
}
  • this logic can be implemented without importing the package that defines the error or indeed knowing anything about err‘s underlying type (このロジックは error を定義している package を import することなく 、実際に err の基になる 型 について何も知らなくても 実装できます)
    • we're simply interested in its behaviour. (単にその動作に興味があります)

 

補足: 「 プログラミング言語Go 」 の 第7章 「 7.12 インタフェース型アサーションによる振る舞いの問い合わせ 」 io.WriteString の話

error の話から少しずれますが、
The Go Programming Language の 日本語版 プログラミング言語Go の 第7章(P241)
io.WriteString の実装 の話が出ていて、
上記 Assert errors for behaviour, not type の話と似て いたので紹介.

io.WriteString の実装

type StringWriter interface {
	WriteString(s string) (n int, err error)
}

func WriteString(w Writer, s string) (n int, err error) {
	if sw, ok := w.(StringWriter); ok {
		return sw.WriteString(s)
	}
	return w.Write([]byte(s))
}

以下、 The Go Programming Language の 日本語版 プログラミング言語Go の 第7章(P241) から一部引用:

*bytes.Buffer, *os.File, *bufio.Writer を含めた io.Writer を満足する多数の重要な型も WriteString メソッドを持っています

io.Writer である wどれもが WriteString メソッドも持っていると想定することはできません

しかし、このメソッドだけを持っている新たなインタフェースを定義して、 w の動的な型 がそのインタフェースを満足しているかどうかを検査するために型アサーションを使うことができます

w(io.Writer) の基になり、 WriteString を実装 している 型(*bytes.Buffer, *os.File:, *bufio.Writer:, ...) について何も知らなくても 実装できる
つまり、以下のように書かなくて済む

func WriteString(w io.Writer, s string) (n int, err error) {
	switch w := w.(type) {
	case *bytes.Buffer
		w.WriteString(s)
	case *os.File:
		w.WriteString(s)
	case *bufio.Writer:
		w.WriteString(s)
	case *(中略...):
		w.WriteString(s)
	default:
		w.Write([]byte(s))
	}
}

 


Go 1.13 からのエラー処理

errors - The Go Programming Language

errors - The Go Programming Language

A simple way to create wrapped errors is to call fmt.Errorf and apply the %w verb to the error argument:

errors.Unwrap(fmt.Errorf("... %w ...", ..., err, ...))
if errors.Is(err, os.ErrExist)
var perr *os.PathError
if errors.As(err, &perr) {
	fmt.Println(perr.Path)
}

WEB+DB PRESS Vol.112|技術評論社

WEB+DB PRESS Vol.112|技術評論社

Goに入りては…… ── When In Go...
【第14回】Go 1.13からのエラー処理 ……標準ライブラリのみでエラー箇所を伝える

 

※今回はここまで

次回 -> 第4回(2020/01/08) 実施です (第4回 予習メモ)


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment