Join Weeyble on Slack. ( weeyble.slack.com )
事前に登録をお願いします
channel: #go
ローカル環境でGo言語が動作すること
https://github.com/quii/learn-go-with-tests
- 前回(2019/11/20) 未参加で、今回が初参加の方はいますか?
- ローカル環境でGo言語が動作する状態ですか?
- Go言語を動かしたことはありますか?
- Go言語を実務で経験している方はいますか?
- むしろ教えて下さい...
- つっきー さん
- @ohtsuchi
- 経歴: フリーランス の Java サーバサイド 開発者
- Go言語の実務経験は無し...
- 保有資格: Spring Professional v4.2, AWS Certified Solutions Architect - Associate, CKAD: Certified Kubernetes Application Developer, etc...
- 経歴: フリーランス の Java サーバサイド 開発者
https://github.com/quii/learn-go-with-tests
Table of contents
のGo fundamentals
の欄1. Install Go
から17. Maths
まで- 前回(2019/11/20) は
2. Hello, world
まで終了
- 前回(2019/11/20) は
- さらに その下の
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では
- Hello, World
- 定番
"Hello, world"
から始まって (hello.go
) go run hello.go
実行
- 定番
- How to test
separate your "domain" code from the outside world (side-effects)
("domain" code を 外の世界(side effect
(副作用))から分離する)fmt.Println
はside effect
(副作用)stdout
(標準出力) に出力するため
- 戻り値で
string
を返す関数を作成 ->Hello()
- test code (
hello_test.go
) 追加 go test
実行
- Writing tests
- ファイルの名:
xxx_test.go
- 関数名 =
Test
から始まる - 関数の引数 =
t *testing.T
- ファイルの名:
if
- 補足: A Tour of Go: If 参照
> 括弧 ( ) は不要で、中括弧 { } は必要です
- 補足: A Tour of Go: If 参照
- Declaring variables
:=
- 補足: A Tour of Go: Short variable declarations 参照
> 関数の中では、 var 宣言の代わりに、短い := の代入文を使い、暗黙的な型宣言ができます
> 関数の外では、キーワードではじまる宣言( var, func, など)が必要で、 := での暗黙的な宣言は利用できません
- 補足: A Tour of Go: Short variable declarations 参照
t.Errorf
print out a message and fail the test
(メッセージを print して、テストに失敗します)- 補足: testing パッケージ - golang.jp 参照
> Errorfは、Logf()とFail()を呼び出すことと同じです
> Logfは、Printf()と同じように、引数をそれぞれに指定した書式で書式化し、テキストをエラーログに記録します
> Failは、テスト関数でエラーがあったが実行は継続したことを記録します
%q
wraps your values in double quotes
(値を""で囲みます)
- Go doc
- Hello, YOU
- next requirement: 引数追加
- test code を先に書いて
go test
- コンパイルエラー
- コンパイルエラー 解消のための 最小限の実装
go test
- 期待値が異なるので テスト失敗
- test pass するための正しい実装
go test
-> test pass
- Constants (定数化)
const
- 補足: A Tour of Go: Constants 参照
> 定数は、文字(character)、文字列(string)、boolean、数値(numeric)のみで使えます
> 定数は := を使って宣言できません
- Hello, world... again
t.Run
- subtests
- test を group 化
describing different scenarios
(異なるシナリオを記述します)
- リファクタリング: assertion 部分を 関数 として抽出
In Go you can declare functions inside other functions and assign them to variables
(Goでは、他の関数内で関数を宣言して変数に割り当てることができます)
t.Helper()
- テスト失敗時に報告される 行数 が helper 関数 内ではなく、 helper 関数を呼び出している側の 行数 となる
func TestHello(t *testing.T) {
assertCorrectMessage := func(t *testing.T, got, want string) {
t.Helper()
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
// 以下略
}
- Discipline
- Keep going! More requirements
- 多言語対応. 第2引数追加
switch
- 補足: A Tour of Go: Switch 参照
- one...last...refactor?
- リファクタリング:
switch
で言語判定している部分を 関数 として抽出 named return value
とnaked return
を使用- 補足: A Tour of Go: Basics - Named return values 参照
> この戻り値の名前は、戻り値の意味を示す名前とすることで、関数のドキュメントとして表現するようにしましょう
> return ステートメントに何も書かずに戻すことができます。これを "naked" return と呼びます
> naked returnステートメントは、短い関数でのみ利用すべきです
- 補足: A Tour of Go: Basics - Named return values 参照
- リファクタリング:
func greetingPrefix(language string) (prefix string) {
switch language {
case french:
prefix = frenchHelloPrefix
// 中略
}
return
}
named return value -> prefix
naked return -> return
単独. ( naked return を利用しない書き方=return prefix
も可能 )
- Integers
integers
ディレクリ作成- ディレクトリ移動
cd ../integers
- ディレクトリ移動
adder_test.go
作成Go source files can only have one package per directory,
(Go ソースファイルには、ディレクトリごとに 1つのpackage
しか含めることができません。)- -> Five suggestions for setting up a Go project | Dave Cheney ※後述
- Write the test first
%d
- 10進数表記
note that we are no longer using the main package
(main package を使用していないことに注意)instead we've defined a package named integers,
(代わりに integers という名前の package を定義)
- Try and run the test
go test
実行- コンパイルエラー
- Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
- コンパイルを通すための(しかし test は失敗する)コードを記述
(x, y int)
(x int, y int)
を短くできる- 「同じ型」の引数が複数並んでいる場合
- 補足: A Tour of Go: Functions continued 参照
go test
adder_test.go:10: expected '4' but got '0'
- 期待値が異なるので テスト失敗
the test is correctly reporting what is wrong.
(test は何が間違っているかを正しく報告)
named return value
aren't using the same here.
(ここでは同じものを使用していません)It should generally be used when the meaning of the result isn't clear from context,
(戻り値 の意味が context から明らかにならない時に使用すべき)in our case it's pretty much clear that Add function will add the parameters
(この場合、Add
関数が引数を追加することはかなり明らかです)
- -> CodeReviewComments · golang/go Wiki ※後述
- Write enough code to make it pass
go test
-> test pass
- v1 終了
- Refactor
- 関数にコメント追加
- Go Doc で表示される
- Examples
ExampleAdd
関数 をadder_test.go
に追加go test -v
- コメント(
"//Output: 6"
) を消すとExampleAdd
関数 は実行されない
- コメント(
- example は
godoc
で確認できる - local で確認
godoc -http=:6060
実行 ->http://localhost:6060/pkg/
- public URL
func ExampleAdd() {
sum := Add(1, 5)
fmt.Println(sum)
// Output: 6
}
- v2 終了
以下 3. Integers から リンクされていた記事の紹介
Five suggestions for setting up a Go project | Dave Cheney
The name of the package should match the name of the directory
(package の名前は、ディレクトリの名前と一致 する必要があります)- 補足: A Tour of Go: Packages 参照
> パッケージ名はインポートパスの最後の要素と同じ名前になります
> インポートパスが "math/rand" のパッケージは、 package rand ステートメントで始まるファイル群で構成します
> (もしURLを含むインポートパスが "golang.org/x/net/websocket" だった場合は、 package websocket になります)
- 補足: A Tour of Go: Packages 参照
Package names should be all lower case.
(package 名はすべて小文字)should contain only letters, numbers
(文字、数字のみ)absolutely no punctuation.
(句読点は絶対に含めない)
The name of a package is part of the name of every type, constant, variable, or function
(package 名は、すべての型、定数、変数、または関数の名前の一部です)Avoid repetition.
(繰り返しを避ける)- (❌)
bytes.BytesBuffer
,strings.StringReader
- (⭕)
bytes.Buffer
,strings.Reader
- (❌)
- For more advice on naming -> What's in a name? ※後述
All the files in a package’s directory must have the same package declaration,
(パッケージのディレクトリ内の すべてのファイルは同じ package 宣言 を持つ必要があります)with one exception.
(1つの例外を除いて)your test files, ... may declare themselves to be in the same package,
(test file は同じ package にあることを宣言できます)but with _test appended to the package declaration.
(しかし_test
を package 宣言に追加 できます)- ->
external test
- 補足: 「循環参照」を避ける場合や「Exampleテスト」の時に使用するようです。以下を参照
- Golang テスト実行時の循環参照でハマった話 - Qiita
- 外部テストパッケージの利用ケース #golang - Qiita
you can’t put the code for multiple packages into one directory.
(複数の package のコードを1つのディレクトリに配置できません)
Main packages deviate from the previous statement about the package declaration and the packages’ directory being the same.
(main package は「package 宣言と package のディレクトリが同じであるという前の文」からは外れます)the name of the command is taken from the name of the package’s directory.
(command の名前 は package の ディレクトリの名前から取得 されます)This obviates the need to use flags like -o when building
(これにより build 時に-o
などのフラグを使用する必要がなくなります)
例) hello
ディレクトリ 直下に main
関数 を定義している hello.go
が存在する状態で go build
を実行
% pwd
/<中略>/learn-go-with-tests/hello
% ls
hello.go hello_test.go
% cat hello.go
package main
(※中略)
func main() {
fmt.Println(Hello("world", ""))
}
% go build
実行可能ファイル hello
が作成されている -> ./hello
で実行
% ls -F
hello* hello.go hello_test.go
% ./hello
Hello, world
hello
-> helloooo
にディレクトリの名前を変更 してから再び go build
を実行
% cd ..
% mv hello helloooo
% cd helloooo
% go build
実行可能ファイル helloooo
が作成されている -> ./helloooo
で実行
% ls -F
hello* hello.go hello_test.go helloooo*
% ./helloooo
Hello, world
The go commands ... all work with packages
(goコマンドはすべてパッケージで動作します)go build
go install
go test
go get
go run is the exception to this rule
(go run
は この規則の例外です)
The import path is effectively the full path to your package.
(import パスは、実質的に package へのフルパス です)Note: There is no concept of sub packages in Go.
(注: Go には sub package の概念はありません)
In Go, the convention is to include the location of the source code
(Goでは、ソースコードの場所を含めるのが慣例です)github.com/golang/glog
- A single package
- Multiple packages
- etc...
以下 Five suggestions for setting up a Go project | Dave Cheney から リンクされていた記事の紹介
- p5. MixedCase
- MixedCase を使う
- アンダースコア(
_
)区切りは❌ - acronym(頭字語.
URL
,HTTP
,ID
など) は大文字のみ
- p6 〜 p8. local 変数
- local 変数 は短く!!
i
,r
,b
の方が良いindex
,reader
,buffer
よりも
- 例)
RuneCount
という名前の関数内でcount
の方が良いruneCount
よりも
- 関数名から
rune
のcount
なのは明らか
- p9. 引数
- 引数 も local 変数 と同様
- しかし Go Doc で表示される
- 型 が descriptive(説明的な) な場合は 変数名 を 短くすべき
- 例)
Duration
の 変数名 =d
duration
ではなく- 型から「期間」なのが明確
- 例)
- 型 が
int64
,[]byte
など ambiguous(曖昧)な場合は 変数名 を documentation として提供- 例)
func HasPrefix(s, prefix []byte) bool
- 第2引数 が
prefix
- 第1引数 の
s
が 検索対象の文字列なのが分かる
- 第2引数 が
- 例)
- p10. 戻り値
- 先頭大文字の関数(exported function) の 戻り値
- ドキュメント化のためにのみ、名前を付ける必要
- 先頭大文字の関数(exported function) の 戻り値
- p11 Receiver の 変数名
- receiver の 型の名前 の 先頭1文字 or 先頭2文字
- p13 Interface の 型名
- メソッド名 +
er
Read()
を定義している Interface ->Reader
- 英語として間違っていても、そのまま
er
を付けて良いExec()
を定義している Interface ->Execer
- 辞書で
execer
を検索しても出てこない
- 辞書で
- 例外:
ReadByte()
を定義している Interface ->ByteReader
Read
とByte
が逆
- 複数のメソッド を定義している Interface の場合
choose a name that accurately describes its purpose
(目的を正確に説明する名前を選択してください)- 例)
ResponseWriter
,ReadWriter
- メソッド名 +
以下 3. Integers から リンクされていた記事の紹介
Named Result Parameters - CodeReviewComments · golang/go Wiki
func (n *Node) Parent1() *Node
(名前無し) の方が良いfunc (n *Node) Parent1() (node *Node)
よりもNode
という型名で分かるし、戻り値が1個だけだし
if a function returns two or three parameters of the same type, or if the meaning of a result isn't clear from context, adding names may be useful in some contexts.
(関数が同じ型の2つまたは3つのパラメーターを返す場合、または結果の意味がコンテキストから明確でない場合、名前を追加することが一部のコンテキストで役立つ場合があります)- ->
func (f *Foo) Location() (lat, long float64, err error)
の方が clearfunc (f *Foo) Location() (float64, float64, error)
よりもlatitude
(緯度),longitude
(経度) を返す事が明確
- ->
Don't name result parameters just to avoid declaring a var inside the function;
(関数内でvarを宣言しないようにするためだけに、結果パラメーターに名前を付けないでください。)- 楽をするため(変数宣言を省略)だけに使ってはいけない
Naked returns are okay if the function is a handful of lines.
(関数が少数の行であれば、 Naked returns は問題ありません)in some cases you need to name a result parameter in order to change it in a deferred closure
(場合によっては、遅延クロージャーで変更するために結果パラメーターに名前を付ける必要があります)defer
の説明は12. Select
で出てきます- 補足: 5 More Gotchas of Defer in Go — Part II - Learn Go Programming 参照
- Iteration
for
のみwhile
,do
,until
は Go には存在しない
- Write the test first
iteration
ディレクリ作成- ディレクトリ移動
cd ../iteration
- ディレクトリ移動
repeat_test.go
作成
- Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
repeat.go
作成
- Write enough code to make it pass
for
()
無し{}
必須- -> Go by Example: For
- ※
Go by Example
の 各ページ の 「ソースコード欄の右上の 」をクリックすると、The Go Playground
に飛びます
- ※
- 補足: A Tour of Go: For 参照
var repeated string
- 変数の宣言のみ
- ゼロ値(
string
の場合は""
)で初期化される - -> Go by Example: Variables
- 補足: A Tour of Go: Variables 参照
> var ステートメントはパッケージ、または、関数で利用できます
- v1 終了
- Refactor
+=
- repeatCount を const 化
- v2 終了
- Benchmarking
func BenchmarkXxx(*testing.B)
をrepeat_test.go
に追記b.N
the framework will determine what is a "good" value
(framework は "good" value を決定します)
go test -bench=.
実行
- vx 終了
exercises (1) Change the test so a caller can specify how many times the character is repeated
(テストを変更して、呼び出し元が文字の繰り返し回数を指定できるように)
- Try and run the test
- test code (
repeat_test.go
) から修正 Repeat()
の 第2引数 を追加- 後の step で テスト失敗 させたいので 期待値(
expected
) も変更
- test code (
func TestRepeat(t *testing.T) {
- repeated := Repeat("a")
- expected := "aaaaa"
+ repeated := Repeat("a", 6)
+ expected := "aaaaaa"
func BenchmarkRepeat(b *testing.B) {
for i := 0; i < b.N; i++ {
- Repeat("a")
+ Repeat("a", 6)
go test
- コンパイルエラー
% go test
./repeat_test.go:6:20: too many arguments in call to Repeat
have (string, number)
want (string)
./repeat_test.go:16:9: too many arguments in call to Repeat
have (string, number)
want (string)
- Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
repeat.go
修正Repeat()
の 第2引数 を追加
-func Repeat(character string) string {
+func Repeat(character string, count int) string {
go test
- テスト失敗
% go test
--- FAIL: TestRepeat (0.00s)
repeat_test.go:10: expected "aaaaaa" but got "aaaaa"
- Write enough code to make it pass
repeat.go
修正
-const repeatCount = 5
-
func Repeat(character string, count int) string {
var repeated string
- for i := 0; i < repeatCount; i++ {
+ for i := 0; i < count; i++ {
repeated += character
}
go test
% go test
PASS
- Try and run the test
- test code (
repeat_test.go
) に追記 3. Integers
からコピペ
- test code (
func ExampleAdd() {
sum := Add(1, 5)
fmt.Println(sum)
// Output: 6
}
↓
- Write enough code to make it pass
func ExampleRepeat() {
repeated := Repeat("a", 6)
fmt.Println(repeated)
// Output: aaaaaa
}
go test -v
exercises (3) Have a look through the strings package
Find functions you think could be useful and experiment with them by writing tests like we have here.
(役に立つと思われる関数を見つけ、ここにあるようなテストを書いて試してください)- Try and run the test
- test code (
repeat_test.go
) から修正 TestRepeat2()
新規追加
- test code (
func TestRepeat2(t *testing.T) {
repeated := Repeat2("a", 6)
expected := "aaaaaa"
if repeated != expected {
t.Errorf("expected %q but got %q", expected, repeated)
}
}
go test
- コンパイルエラー
./repeat_test.go:30:14: undefined: Repeat2
- Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
repeat.go
追記Repeat2()
新規追加
func Repeat2(character string, count int) string {
return ""
}
go test
- テスト失敗
% go test
--- FAIL: TestRepeat2 (0.00s)
repeat_test.go:34: expected "aaaaaa" but got ""
- Write enough code to make it pass
repeat.go
修正strings
package のfunc Repeat(s string, count int) string
を使用
+import "strings"
+
func Repeat2(character string, count int) string {
- return ""
+ return strings.Repeat(character, count)
go test
% go test
PASS
- Write the test first
arrays
ディレクトリ作成- ディレクトリ移動
cd ../arrays
- ディレクトリ移動
sum_test.go
作成- array の書き方 2種類
[N]type{value1, value2, ..., valueN}
[...]type{value1, value2, ..., valueN}
%v
- "default" format
- -> fmt - The Go Programming Language
> The default format for %v is:
int
は%d
,string
は%s
, ...
- Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
- Write enough code to make it pass
- v1 終了
- Refactor
range
を使用して array をループ- 1個目の戻り値: index
- 2個目の戻り値: value
- 補足: A Tour of Go: Range 参照
> rangeは反復毎に2つの変数を返します
> 1つ目の変数はインデックス( index )で
> 2つ目はインデックスの場所の要素のコピーです
_
- index は使用しないので、1個目の戻り値を無視
- 補足: A Tour of Go: Range continued 参照
> インデックスや値は、 " _ "(アンダーバー) へ代入することで捨てることができます
> もしインデックスだけが必要なのであれば、 " , value " を省略します
- v2 終了
- Arrays and their type
An interesting property of arrays is that the size is encoded in its type.
(配列の興味深い特性は size が その型の中に エンコードされる ことです)
- Write the test first
- use the
slice
type- array
myArray := [3]int{1,2,3}
ではなく - slice
mySlice := []int{1,2,3}
の テストケース 追加
- array
- use the
- Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
- 対応策は 以下のいずれか
- (1)
Break the existing API by changing the argument to Sum to be a slice rather than an array
(Sum
の引数をarray
ではなくslice
に変更して、既存のAPIを破壊します) - (2) Create a new function
- (1)
- 今回は前者の「既存のAPIを破壊」を選択
In our case, no-one else is using our function so rather than having two functions to maintain let's just have one.
(この場合、他の誰も関数を使用していないので、2つの関数を維持するのではなく、1つだけにしましょう)
Sum(numbers [5]int)
->Sum(numbers []int)
に変更- 1個目の test (
t.Run("collection of 5 numbers", …
) が コンパイルエラーnumbers := [5]int{1, 2, 3, 4, 5}
->numbers := []int{1, 2, 3, 4, 5}
に変更
- 1個目の test (
- 対応策は 以下のいずれか
- Write enough code to make it pass
- v3 終了
- Refactor
Every test has a cost
In our case, you can see that having two tests for this function is redundant.
(この場合、この関数に対して2つのテストを行うことは冗長であることがわかります)go test -cover
実行coverage: 100.0%
delete one of the tests and check the coverage again.
(テストの1つを削除し、カバレッジを再度確認)- 1個目の test (
t.Run("collection of 5 numbers", …
) を削除 coverage: 100.0%
で変わらない
- 1個目の test (
- next challenge ->
SumAll
新規関数
※今回はここまで
次回 -> 第3回 2019/12/18 実施です (第3回 発表メモ)