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言語を動かしたことはありますか?
- つっきー さん
- @ohtsuchi
- 経歴: フリーランス の Java サーバサイド 開発者
- Go言語の実務経験は無し...
- Go 詳しい方、教えて下さい...
- Go言語の実務経験は無し...
- 保有資格: Spring Professional v4.2, AWS Certified Solutions Architect - Associate, CKAD: Certified Kubernetes Application Developer, etc...
- 使用エディタ
- JetBrains 社の
IntelliJ IDEA Ultimate
+Go
Plugin (GoLand
と同等のはず?)
- JetBrains 社の
- 経歴: フリーランス の Java サーバサイド 開発者
- 次回(第4回) は つっきー さんが担当するそうです
- 別資料は作る必要なし
- 前回(第2回) と 今回 は箇条書きレベルのメモを作成しましたが、特に別資料を作成しなくても大丈夫です
- 興味ある方は slack で立候補してみて下さい
- いなければ、自分がまた担当したいです
https://github.com/quii/learn-go-with-tests
Table of contents
のGo fundamentals
の欄1. Install Go
から17. Maths
まで
- さらに その下の
Build an application
の欄HTTP server
など、application を作成しながら TDD を学ぶ構成
- 各章毎に、解説とソースコードが並ぶ構成
- 全ての章の ページ先頭 に、ソースファイルが格納されているディレクトリへの リンク(さらにその直下に
v1
,v2
, etc...)- 基本的に解説内にソースコードも記述されていますが、たまに情報不足の時があるのでその時はファイルを確認して下さい
if language == french {
return frenchHelloPrefix + name
}
定数: french
, frenchHelloPrefix
の定義部分が載っていない
v6
のソースファイル で確認
// 前略
const french = "French"
// 中略
const frenchHelloPrefix = "Bonjour, "
// 以下略
it's up to you how you structure your folders.
(フォルダをどのように構成するかはあなた次第です)- 最新のGoでは
Go Modules
を使用できますが、 本教材では$GOPATH/src
以下のディレクトリ を使用 しているのでそれに合わせます- Go Modules については以下の記事など参照
- Go Modules - Qiita
- Go 1.13 に向けて知っておきたい Go Modules とそれを取り巻くエコシステム - blog.syfm
> 以前 *1 は Go 1.13 から Go modules がデフォルト ($GO111MODULE=on) になるとアナウンスされていましたが、Issue#31857 にて auto をデフォルトのままとする決定が行われました
"learn-go-with-tests/hello"
- 自分は上記のディレクリ構成で進めます
- 最新のGoでは
前回 の 発表メモ
- 前回:
5. Arrays and slices
の 2回目のRefactor
(coverage の確認)まで終了 - 今回:
5. Arrays and slices
の 3回目のWrite the test first
(SumAll
関数の test 作成) から
- 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 (可変長引数の関数)
...
- slice として受け取る
- -> Go by Example: Variadic Functions
- ※
Go by Example
の 各ページ の 「ソースコード欄の右上の 」をクリックすると、The Go Playground
に飛びます
- ※
...[]int
[][]int
と同じ- 補足: A Tour of Go: Slices of slices 参照
- コンパイル失敗
sum_test.go
のif 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"
slice
とstring
との比較 -> コンパイル できてしまう
- 補足: DeepEqual の定義:
func DeepEqual(x, y interface{}) bool
- 補足: A Tour of Go: The empty interface
> ゼロ個のメソッドを指定されたインターフェース型は、 空のインターフェース と呼ばれます
> 空のインターフェースは、任意の型の値を保持できます
- 補足: 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
sums := make([]int, lengthOfNumbers)
- new way to create a slice.
- 補足: A Tour of Go: Creating a slice with make 参照
- slice 以外にも map と chan で利用
- 後の章: 8. Maps, 11. Concurrency, 15. Context で出てきます
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 Devs の
Using "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 ->
SumAll
をSumAllTails
に変更Tail
= 各 slice の先頭1件目を除いた、2件目以降
- v5 終了
- Write the test first
sum_test.go
の tetTestSumAll
をTestSumAllTails
に変更- test 関数名:
TestSumAll
->TestSumAllTails
- test の中で呼び出す 関数名:
SumAll
->SumAllTails
- 期待値:
want := []int{3, 9}
->[]int{2, 9}
- test 関数名:
- 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
のロジックから変更していないので、全ての値の合計値が返る
- Rename the function to
- Write enough code to make it pass
for
の中身を変更- 変更前
sums = append(sums, Sum(numbers))
- 変更後
tail := numbers[1:]
sums = append(sums, Sum(tail))
- 変更前
slice[low:high]
:
の両サイドを省略可numbers[1:]
"take from 1 to the end"
1
(=2個目) から 末尾まで
- 補足: A Tour of Go: Slice defaults 参照
- 補足: A Tour of Go: Slice length and capacity 参照
- 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
- もし
- repeated code -> extract that into a function
- v7 終了
- 注:
t.Helper()
が抜けている? ので注意
- 注:
- Wrapping up
- -> Go Slices: usage and internals - The Go Blog ※後述
- -> Here is an example of slicing an array ※後述
- -> Another example ※後述
以下 5. Arrays and slices から リンクされていた記事の紹介
Go Slices: usage and internals - The Go Blog
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 blog の Append
参照
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
も生き続ける
a
と b
とは 別の配列を指す c
にデータを copy
して、
以後は c
だけ使用 して、 且つ a
と b
が スコープ外になれば、
a
と b
が指す 100万個 は garbage の対象 となる. という意味のはず...
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
- 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
と呼ばれる 独自の型を定義 する)
- (1)
- 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 passpassing 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
- -> fmt - The Go Programming Language
- 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 を定義
- (1) 別の package で定義 ->
- What are methods?
- test 変更. メソッド 呼び出しに変更
got := Area(rectangle)
->got := rectangle.Area()
got := Area(circle)
->got := circle.Area()
go test
rectangle.Area undefined
circle.Area undefined
- メソッド 未定義なので コンパイルエラー
- test 変更. メソッド 呼び出しに変更
- 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
変数 -> 他の言語だとthis
やself
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
Rectangle
とCircle
のArea
メソッド 正しい実装に変更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)
})
↓ Circle
を checkArea
の 第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" キーワードは必要ありません
- 補足: A Tour of Go: Interfaces are implemented implicitly 参照
- 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 終了
- 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 を持つべき です)
- The Go Programming Language - Alan A. A. Donovan, Brian W. Kernighan - Google ブックス 参照
func (w *Wallet) Deposit(amount int) {
w.balance += amount
}
func (w *Wallet) Balance() int {
return w.balance
}
- ???? (続き)
go test
-> test passw.balance
- automatically dereferenced
- pointer(
w
) を 明示的に dereference ((*w).balance
) しなくても、自動でしてくれる
- pointer(
- automatically dereferenced
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
を返す
- ->
- 重複部分 -> Helper 関数 (
- v2 終了
- Write the test first
TestWallet
関数 内にt.Run("Withdraw insufficient funds", ...
追加- 残高よりも多く引き出そうとした時に
Withdraw
がerror
を返す 事を確認- 戻り値の
error
はnil
になる可能性error
はinterface
のため
error
がnil
かどうかチェック する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
) 抽出
- Helper 関数 (
- 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 で修正 されます
- 第2引数 引数名 変更
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 に対して グローバルな値を定義することができます)- 補足: ちなみに
var
をconst
に変更すると コンパイルエラー- 補足: A Tour of Go: Constants 参照
> 定数は、文字(character)、文字列(string)、boolean、数値(numeric)のみで使えます
- 次章 8. Maps で
string
を元 にtype
を定義した後にerror
を実装 してconst
化 する手順を学びます
- 補足: A Tour of Go: Constants 参照
- 補足: ちなみに
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
- 補足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
と同様の機能が追加されたので、 今後はそちらの方を利用した方が良いのかも?
Go's error handling can be classified into the three core strategies.
(Go の error handling は、 3つのコア戦略 に分類できます)
if err == ErrSomething { … }
- 例:
io.EOF
- 補足: io - The Go Programming Language: Variables 参照
var EOF = errors.New("EOF")
- 補足: io - The Go Programming Language: Variables 参照
the caller must compare the result to predeclared value using the equality operator.
(呼び出し側 は 等値演算子 を使用 して 結果を事前宣言された値と比較 する必要がある)
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 & errors の assertError
で Error()
の エラーメッセージ の 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 に変更 したのと同じ事が書かれています
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 しなければなりません)
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 から余分なコンテキストを抽出できます)- 補足: A Tour of Go: Type assertions 参照
t, ok := i.(T)
- 補足: A Tour of Go: Type switches 参照
switch v := i.(type) {
- 補足: A Tour of Go: Type assertions 参照
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 を提供できることです)
creates a strong coupling with the caller, making for a brittle API.
(呼び出し元との強い結合を作成 し、脆弱な API を作成します)
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
}
一旦飛ばして後述します.
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 のコンテキストが破壊されます)
I’d like to suggest a method to add context to errors
(error に context を追加 する方法を提案したい)
// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
// Cause unwraps an annotated error.
func Cause(err error) error
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
(以下略)
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.
(単にその動作に興味があります)
error の話から少しずれますが、
The Go Programming Language の 日本語版 プログラミング言語Go の 第7章(P241) で
io.WriteString
の実装 の話が出ていて、
上記 Assert errors for behaviour, not type
の話と似て いたので紹介.
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))
}
}
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)
}
Goに入りては…… ── When In Go...
【第14回】Go 1.13からのエラー処理 ……標準ライブラリのみでエラー箇所を伝える
※今回はここまで
次回 -> 第4回(2020/01/08) 実施です (第4回 予習メモ)