Open7

Go をざっくりとキャッチアップ

zuribozuribo

チュートリアルをやる (1/N)

https://go.dev/doc/tutorial/

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.
zuribozuribo

チュートリアルをやる (2/N)

Create a Go module

https://go.dev/doc/tutorial/create-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!]
zuribozuribo

基本的なパッケージ

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
}
zuribozuribo

Testing in Go

  • go test コマンドを実行すると、*_test.go ファイルが検索されて実行される。
  • go test <package> コマンドで、特定のパッケージのテストのみを実行できる。
  • *_test.go ファイルは、本体のコードと同じディレクトリ内に配置するの必要がある。
  • テストの関数名は Test から始まる必要がある (e.g. TestXyzTest_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
zuribozuribo

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

参考リンク

zuribozuribo

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]