Go をざっくりとキャッチアップ
Go とは
- Go は Google が開発した静的型付き、コンパイル言語である。
- C とシンタックスは似ているが、メモリ安全性やガベージコレクションなどのサポートしている。
チュートリアルをやる (1/N)
Getting started
Hello, World!
hello ディレクトリを作成する。
$ mkdir hello
$ cd hello
依存関係のトラッキングを有効にする。
go mod init <module-path>
の <module-path>
には、モジュールのソースコードが置かれる場所を指定する (例えば、github.com/mymodule
) 。
$ go mod init example/hello
$ cat go.mod
module example/hello
go 1.21.5
hello.go
ファイルを作成する。
-
main
パッケージを宣言する (パッケージは関数をまとめる方法で、同じディレクトリ内の全てのファイルで構成される) -
fmt
パッケージをインポートする。 -
main
関数で標準出力に "Hello, World!" を出力する。
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
実行する。
$ go run .
Hello, World!
外部のパッケージのコードを呼び出す
pkg.go.dev で "quote" を検索して、rsc.io/quote
を見つけ出す。
rsc.io/quote
をインポートして、コードを以下のように変更する。
$ cat hello.go
package main
import "fmt"
import "rsc.io/quote"
func main() {
fmt.Println(quote.Go())
}
依存関係の追加する。
$ go mod tidy
go: finding module for package rsc.io/quote
go: downloading rsc.io/quote v1.5.2
go: found rsc.io/quote in rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
$ cat go.mod
module example/hello
go 1.21.5
require rsc.io/quote v1.5.2
require (
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
rsc.io/sampler v1.3.0 // indirect
)
$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
実行する。
$ go run .
Don't communicate by sharing memory, share memory by communicating.
チュートリアルをやる (2/N)
Create a Go module
2つのモジュールを作成する。
- library
- caller application
モジュールの作成
モジュールを作成する。
$ mkdir greetings
$ cd greetings
$ go mod init example.com/greetings
go: creating new go.mod: module example.com/greetings
greetings.go
ファイルを作成する。
-
Hello
: 関数名 -
name string
: 引数名と型 -
string
: 返り値の型
$ cat greetings.go
package greetings
import "fmt"
// Hello returns a greeting for the named person.
func Hello(name string) string {
// Return a greeting that embeds the name in a message.
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message
}
他のモジュールから呼び出す
以下のディレクトリ構成にする。
/
|-- greetings/
|-- hello/
$ mkdir hello
$ cd hello
$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
hello.go
ファイルを作成する。
package main
import (
"fmt"
"example.com/greetings"
)
func main() {
// Get a greeting message and print it.
message := greetings.Hello("Gladys")
fmt.Println(message)
}
$ go mod edit -replace example.com/greetings=../greetings
$ cat go.mod
module example.com/hello
go 1.21.5
replace example.com/greetings => ../greetings
$ go mod tidy
go: found example.com/greetings in example.com/greetings v0.0.0-00010101000000-000000000000
$ cat go.mod
module example.com/hello
go 1.21.5
replace example.com/greetings => ../greetings
require example.com/greetings v0.0.0-00010101000000-000000000000
実行する。
$ go run .
Hi, Gladys. Welcome!
エラーハンドリング
greetings/greetings.go
を以下のように変更する。
package greetings
import (
"errors"
"fmt"
)
// Hello returns a greeting for the named person.
func Hello(name string) (string, error) {
// If no name was given, return an error with a message.
if name == "" {
return "", errors.New("empty name")
}
// Return a greeting that embeds the name in a message.
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message, nil
}
hello/hello.go
を以下のように変更する。
package main
import (
"fmt"
"log"
"example.com/greetings"
)
func main() {
// Set properties of the predefined Logger, including
// the log entry prefix and a flag to disable printing
// the time, source file, and line number.
log.SetPrefix("greetings: ")
log.SetFlags(0)
// Request a greeting message.
message, err := greetings.Hello("")
// If an error was returned, print it to the console and
// exit the program.
if err != nil {
log.Fatal(err)
}
// If no error was returned, print the returned message
// to the console.
fmt.Println(message)
}
実行する。
$ go run .
greetings: empty name
exit status 1
ランダムな挨拶を返す
greetings/greetings.go
を以下のように変更。
package greetings
import (
"errors"
"fmt"
"math/rand"
)
// Hello returns a greeting for the named person.
func Hello(name string) (string, error) {
// If no name was given, return an error with a message.
if name == "" {
return "", errors.New("empty name")
}
// Return a greeting that embeds the name in a message.
message := fmt.Sprintf(randomFormat(), name)
return message, nil
}
// randomFormat returns one of a set of greeting messages. The required
// message is selected at random.
func randomFormat() string {
// A slice of message formats.
formats := []string{
"Hi, %v. Welcome!",
"Great to see you, %v!",
"Hail, %v! Well met!",
}
// Return a randomly selected message format by specifying
// a random index for the slice of formats.
return formats[rand.Intn(len(formats))]
}
hello/hello.go
を以下のように変更する。
package main
import (
"fmt"
"log"
"example.com/greetings"
)
func main() {
// Set properties of the predefined Logger, including
// the log entry prefix and a flag to disable printing
// the time, source file, and line number.
log.SetPrefix("greetings: ")
log.SetFlags(0)
// Request a greeting message.
message, err := greetings.Hello("Gladys")
// If an error was returned, print it to the console and
// exit the program.
if err != nil {
log.Fatal(err)
}
// If no error was returned, print the returned message
// to the console.
fmt.Println(message)
}
実行する。
$ go run .
Great to see you, Gladys!
$ go run .
Hi, Gladys. Welcome!
$ go run .
Hi, Gladys. Welcome!
$ go run .
Hail, Gladys! Well met!
複数の人の挨拶を返す
greetings/greetings.go
を以下のように変更する。
package greetings
import (
"errors"
"fmt"
"math/rand"
)
// Hello returns a greeting for the named person.
func Hello(name string) (string, error) {
// If no name was given, return an error with a message.
if name == "" {
return "", errors.New("empty name")
}
// Return a greeting that embeds the name in a message.
message := fmt.Sprintf(randomFormat(), name)
return message, nil
}
// Hellos returns a map that associates each of the named people
// with a greeting message.
func Hellos(names []string) (map[string]string, error) {
// A map to associate names with messages.
messages := make(map[string]string)
// Loop through the received slice of names, calling
// the Hello function to get a message for each name.
for _, name := range names {
message, err := Hello(name)
if err != nil {
return nil, err
}
// In the map, assocaite the retrieved message with
// the name.
messages[name] = message
}
return messages, nil
}
// randomFormat returns one of a set of greeting messages. The required
// message is selected at random.
func randomFormat() string {
// A slice of message formats.
formats := []string{
"Hi, %v. Welcome!",
"Great to see you, %v!",
"Hail, %v! Well met!",
}
// Return a randomly selected message format by specifying
// a random index for the slice of formats.
return formats[rand.Intn(len(formats))]
}
hello/hello.go
を以下のように変更する。
package main
import (
"fmt"
"log"
"example.com/greetings"
)
func main() {
// Set properties of the predefined Logger, including
// the log entry prefix and a flag to disable printing
// the time, source file, and line number.
log.SetPrefix("greetings: ")
log.SetFlags(0)
// A slice of names.
names := []string{"Gladys", "Samantha", "Darrin"}
// Request greeting messages for the names.
messages, err := greetings.Hellos(names)
if err != nil {
log.Fatal(err)
}
// If no error was returned, print the returned map of
// messages to the console.
fmt.Println(messages)
}
実行する。
$ go run .
map[Darrin:Great to see you, Darrin! Gladys:Hi, Gladys. Welcome! Samantha:Great to see you, Samantha!]
基本的なパッケージ
fmt
フォーマット文字列を扱うためのパッケージ
path/filepath
ファイルパスの操作を行うためのパッケージ
package main
import (
"path/filepath"
)
func main() {
path := "/path/to/dir/filename.txt"
extname := filepath.Ext(path)
println(extname) // .txt
basename := filepath.Base(path)
println(basename) // filename.txt
dirname := filepath.Dir(path)
println(dirname) // /path/to/dir
dirname, basename = filepath.Split(path)
println(basename) // filename.txt
println(dirname) // /path/to/dir/ (Dir と違って最後に / がつく)
joined := filepath.Join("/path/to", "dir/filename.txt")
println(joined) // /path/to/dir/filename.txt
}
Testing in Go
-
go test
コマンドを実行すると、*_test.go
ファイルが検索されて実行される。 -
go test <package>
コマンドで、特定のパッケージのテストのみを実行できる。 -
*_test.go
ファイルは、本体のコードと同じディレクトリ内に配置するの必要がある。 - テストの関数名は
Test
から始まる必要がある (e.g.TestXyz
、Test_xyz
) 。 -
testing
をインポートし、テストの関数の引数に*testing.T
が必要である。
Hands-On
シンプルなテスト
calc.go
package calc
func sum(a, b int) int {
return a + b
}
calc_test.go
import "testing"
func TestSample(t *testing.T) {
expect := "a"
actual := getData()
if actual != expect {
t.Errorf(`expect="%s" actual="%s"`, expect, actual)
}
}
テスト実行
$ go test
PASS
ok calc 0.216s
$ go test -v
=== RUN TestSum
--- PASS: TestSum (0.00s)
PASS
ok calc 0.128s
テーブル駆動テスト
複数のテストケース(名前、入力、出力)をテーブルにまとめておき、テストを実行する。
テストコードとテストデータを分離した形で書くことができ、複数のテストデータに対してテストコードを再利用することができる。
calc_test.go
失敗時の挙動をみたいので、わざと失敗するテストを混ぜておく。
package calc
import "testing"
type Test struct {
name string
input1 int
input2 int
expected int
}
func TestSum(t *testing.T) {
tests := map[string]struct {
input1 int
input2 int
expected int
}{
"1+2=3": { 1, 2, 3 },
"0+0=0": { 0, 0, 0 },
"-1+2=1": { -1, 2, 2 },
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
actual := sum(tt.input1, tt.input2)
if actual != tt.expected {
t.Errorf(`actual = %v, expected = %v`, actual, tt.expected)
}
})
}
}
テスト実行
$ go test -v
=== RUN TestSum
=== RUN TestSum/1+2=3
=== RUN TestSum/0+0=0
=== RUN TestSum/-1+2=1
calc_test.go:27: actual = 1, expected = 2
--- FAIL: TestSum (0.00s)
--- PASS: TestSum/1+2=3 (0.00s)
--- PASS: TestSum/0+0=0 (0.00s)
--- FAIL: TestSum/-1+2=1 (0.00s)
FAIL
exit status 1
FAIL calc 0.662s
テストの並列化
calc_test.go
package calc
import "testing"
type Test struct {
name string
input1 int
input2 int
expected int
}
func TestSum(t *testing.T) {
tests := map[string]struct {
input1 int
input2 int
expected int
}{
"1+2=3": { 1, 2, 3 },
"0+0=0": { 0, 0, 0 },
"-1+2=1": { -1, 2, 2 },
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
t.Parallel()
actual := sum(tt.input1, tt.input2)
if actual != tt.expected {
t.Errorf(`actual = %v, expected = %v`, actual, tt.expected)
}
})
}
}
テスト実行
$ go test -v
=== RUN TestSum
=== RUN TestSum/1+2=3
=== PAUSE TestSum/1+2=3
=== RUN TestSum/0+0=0
=== PAUSE TestSum/0+0=0
=== RUN TestSum/-1+2=1
=== PAUSE TestSum/-1+2=1
=== CONT TestSum/1+2=3
=== CONT TestSum/-1+2=1
=== CONT TestSum/0+0=0
=== NAME TestSum/-1+2=1
calc_test.go:29: actual = 1, expected = 2
--- FAIL: TestSum (0.00s)
--- PASS: TestSum/1+2=3 (0.00s)
--- PASS: TestSum/0+0=0 (0.00s)
--- FAIL: TestSum/-1+2=1 (0.00s)
FAIL
exit status 1
FAIL calc 0.599s
github.com/stretcher/testify/assert
assert
を使うと、もっと簡単にテストが書ける。
calc_test.go
package calc
import (
"testing"
"github.com/stretchr/testify/assert"
)
type Test struct {
name string
input1 int
input2 int
expected int
}
func TestSum(t *testing.T) {
tests := map[string]struct {
input1 int
input2 int
expected int
}{
"1+2=3": { 1, 2, 3 },
"0+0=0": { 0, 0, 0 },
"-1+2=1": { -1, 2, 2 },
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
t.Parallel()
actual := sum(tt.input1, tt.input2)
assert.Equal(t, tt.expected, actual)
})
}
}
テスト実行
$ go test -v
=== RUN TestSum
=== RUN TestSum/-1+2=1
=== PAUSE TestSum/-1+2=1
=== RUN TestSum/1+2=3
=== PAUSE TestSum/1+2=3
=== RUN TestSum/0+0=0
=== PAUSE TestSum/0+0=0
=== CONT TestSum/-1+2=1
=== CONT TestSum/0+0=0
=== NAME TestSum/-1+2=1
calc_test.go:31:
Error Trace: /Users/itazur/workplace/test/go/calc/calc_test.go:31
Error: Not equal:
expected: 2
actual : 1
Test: TestSum/-1+2=1
=== CONT TestSum/1+2=3
--- FAIL: TestSum (0.00s)
--- FAIL: TestSum/-1+2=1 (0.00s)
--- PASS: TestSum/0+0=0 (0.00s)
--- PASS: TestSum/1+2=3 (0.00s)
FAIL
exit status 1
FAIL calc 0.138s
github.com/stretchr/testify/mock
テストを実行したい対象が、外部のコンポーネントに依存している場合がある(例えば、アプリケーションからデータベースのアクセスが発生する場合など)。
この場合に、その外部のコンポーネントの動作を擬似的に真似る Mock を作ることで、その対象を単体テストでテスト可能にすることができる。
main.go
package main
import (
"math/rand"
)
type randNumberGenerator interface {
randomInt(max int) int
}
type standardRand struct {}
func (s standardRand) randomInt(max int) int {
return rand.Intn(max)
}
func divByRand(
numerator int,
r randNumberGenerator,
) int {
denominator := 1 + int(r.randomInt(10))
return numerator / denominator
}
main_test.go
package main
import (
"testing"
"github.com/stretchr/testify/mock"
)
type mockRand struct {
mock.Mock
}
func newMockRand() *mockRand {
return &mockRand{}
}
func (m *mockRand) randomInt(max int) int {
args := m.Called(max)
return args.Int(0)
}
func TestDivByRand(t *testing.T) {
m := newMockRand()
m.On("randomInt", 10).Return(5)
quotient := divByRand(30, m)
if quotient != 5 {
t.Errorf("expected quotient to be 5, got %d", quotient)
}
m.AssertCalled(t, "randomInt", 10)
}
func TestDivByRandCantDivideByZero(t *testing.T) {
m := newMockRand()
m.On("randomInt", 10).Return(0)
quotient := divByRand(30, m)
if quotient != 30 {
t.Errorf("expected quotient to be 30, got %d", quotient)
}
}
テスト実行
$ go test -v
=== RUN TestDivByRand
--- PASS: TestDivByRand (0.00s)
=== RUN TestDivByRandCantDivideByZero
--- PASS: TestDivByRandCantDivideByZero (0.00s)
PASS
ok go_test 0.629s
interface{}
Go 言語では、特定のメソッドを実装していることを保証する型を定義するのに、インターフェースを用いることができる。Rust におけるトレイト (trait) のようなものである。
以下のような形で、複数の型の間で共通するメソッドをインターフェースにまとめておくことができる。これによって、具体的な型は違くても、同じインターフェースを実装している型を1つの配列に入れることができるようになる。
package main
import "fmt"
type Animal interface {
Say() string
}
type Dog struct {}
func (dog Dog) Say() string {
return "Woof woof"
}
type Cat struct {}
func (cat Cat) Say() string {
return "Meow meow"
}
func main() {
animals := []Animal {
Dog{},
Cat{},
}
for _, animal := range(animals) {
fmt.Println(animal.Say())
}
}
$ go run main.go
Woof woof
Meow meow
空のインターフェース
先ほど説明した通り、共通するメソッドをまとめることで、ある種の抽象化を与えるのがインターフェースでした。では、何もメソッドを必要としない空のインターフェース interface {}
の場合はどうでしょうか。
この場合は、任意の型を入れることができるようになります。
例えば、以下のように、浮動小数点を入れたり、整数を入れたり、文字列を入れたりできちゃいます。
package main
import "fmt"
type Any interface {}
func main() {
arr := []Any {1, 0.1, "hello world", 'a'}
for _, i := range arr {
fmt.Println(i)
}
}
$ go run main.go
1
0.1
hello world
97
わざわざ Any
なんてインターフェースを作るのがめんどくさい時は、以下のようにもできます。
package main
import "fmt"
func main() {
arr := []interface{} {1, 0.1, "hello world", 'a'}
for _, i := range arr {
fmt.Println(i)
}
}
map[string] interface{}
ここまで理解できていれば、map[string] interface{}
を理解するのも簡単です。
string
から任意の型へのマップということになります。
これは例えば key-value ペアのデータを扱いたい時などに便利になります。
package main
import "fmt"
func main() {
person := map[string]interface{}{
"name": "zuribo",
"year": 1994,
"hobby": []string {
"programming",
"walking",
"sleeping",
},
}
for key, value := range person {
fmt.Println(key, value)
}
}
$ go run main.go
hobby [programming walking sleeping]
name zuribo
year 1994
参考リンク
Deep Copy & Shallow Copy in Go
Deep Copy
ディープコピーは、a = b
のように記述した際に、実体となるデータをコピーし、それぞれ別々のデータになることを意味します。したがって、一方がデータを変更しても、他方のデータにはその変更は反映されません。
簡単に言えば、以下のような状態です。コピー後に 'b' の値を変更したとしても、'a' の値は変わっていません。
Before deep copy
a
+---+--------------+
| 1 | |
+---+--------------+
After deep copy
a b
+---+------+---+---+
| 1 | | 1 | |
+---+------+---+---+
Change 'b' value
a b
+---+------+---+---+
| 1 | | 2 | |
+---+------+---+---+
Shallow Copy
シャローコピーとは、a = b
のように記述した際に、データをコピー元とコピー先で同じデータを参照しており、データの実体は共有されることを意味します。したがって、一方でデータを変更した場合に、他方のデータにもその変更が反映されます。
簡単に言えば、以下の状態です。コピー後に 'b' を通してデータを変更すると、'a' も同じデータを参照しているので、'a' から見たときもデータの値が変わったことになります。
Before shallow copy
a
+---+--------------+---+---+---+---+
| | | 1 | 2 | 3 | 4 |
+-+-+--------------+---+---+---+---+
| ^
+-----------------+
After shallow copy
a b
+---+--------+---+-+---+---+---+---+
| | | | | 1 | 2 | 3 | 4 |
+-+-+--------+-+-+-+---+---+---+---+
| | ^
+------------+----+
Change value through 'b'
a b
+---+--------+---+-+---+---+---+---+
| | | | | 0 | 2 | 3 | 4 |
+-+-+--------+-+-+-+---+---+---+---+
| | ^
+------------+----+
いつどっちが使われる
どういう時にディープコピーが使われるのか、はたまたシャローコピーが使われるのかは、使用するプログラミング言語に依存します。
ただ、上記の図を見てわかる通り、大枠の基本的な方針としては、別のメモリアドレスを参照している変数を含む変数をコピーする場合には、シャローコピーになります。つまり、=
を用いた時には、その変数内のデータが他の場所のデータを参照しているかどうかを気にせずに、その変数内のデータをそのままコピーする形になるのが一般的です。
ただし、どういう型の時に、別の場所にデータを参照するような形にするのかは、プログラミング言語に依存します。よって、上述のような結論になります。
ただし、一般的な方針としては、コンパイル時にデータのサイズがわからないような型、つまり実行時に動的にサイズが変わる型の場合には、別のメモリ領域を確保してきて、それを参照するようにする形が取られることが多いです。
例えば、配列とスライスはどちらも複数の同じ型のデータを格納するものではありますが、Go では配列はコンパイル時にサイズを指定する一方で、スライスではサイズを指定せずに動的にサイズを変えることが可能なため、配列ではディープコピー、スライスではシャローコピーが使われます。もしスライスでディープコピーを使いたい場合には、コピーできるだけの大きなメモリを確保した上で、copy()
を明示的に呼び出す必要があります。
package main
import "fmt"
func main() {
a1 := [5]int{1, 2, 3, 4, 5}
a2 := a1
a2[0] *= 10
fmt.Printf("a1 = %v\n", a1)
fmt.Printf("a2 = %v\n", a2)
b1 := []int{1, 2, 3, 4, 5}
b2 := b1
b2[0] *= 10
fmt.Printf("b1 = %v\n", b1)
fmt.Printf("b2 = %v\n", b2)
c1 := []int{1, 2, 3, 4, 5}
c2 := make([]int, len(c1))
copy(c2, c1)
c2[0] *= 10
fmt.Printf("c1 = %v\n", c1)
fmt.Printf("c2 = %v\n", c2)
}
$ go run main.go
a1 = [1 2 3 4 5]
a2 = [10 2 3 4 5]
b1 = [10 2 3 4 5]
b2 = [10 2 3 4 5]
c1 = [1 2 3 4 5]
c2 = [10 2 3 4 5]