0
0

learn-go-with-tests マップ

Posted at

マップ

配列とスライスでは、値を順番に格納する方法を見ました。 では、keyでアイテムを保存し、すばやく検索する方法を見てみましょう。

マップを使用すると、辞書と同じようにアイテムを保存できます。 keyは単語、valueは定義と考えることができます。 そして、独自の辞書を構築するよりも、マップについて学ぶより良い方法は何でしょうか?

まず、辞書に定義された単語がすでにあると仮定すると、単語を検索すると、その単語の定義が返されます。

最初にテストを書く

まずテストを書きます。実行すると当然パスしません。
./dictionary_test.go:8:12: undefined: Search

dictionary_test.go
package main

import "testing"

func TestSearch(t *testing.T) {
    dictionary := map[string]string{"test": "this is just a test"}

    got := Search(dictionary, "test")
    want := "this is just a test"

    if got != want {
        t.Errorf("got %q want %q given, %q", got, want, "test")
    }
}

マップはキーとバリューの組み合わせです。map[key]value{"key1": value1}と書きます。

テストを実行して出力を確認するための最小限のコードを記述します

では、最低限のコードを書いてテストを実行します。

dictionary.go
package mapping

func Search(dictionary map[string]string, word string ) string {
	return ""
}

テストには失敗しますが、実行には成功します。

dictionary_test.go:12: got "" want "this is just a test" given, "test"

成功させるのに十分なコードを書く

マップであるdictionaryのkeyで検索します。
これでテストがパスするようになりました。

dictionary.go
func Search(dictionary map[string]string, word string) string {
    return dictionary[word]
}

リファクタリング

まずテストをリファクタリングして、assertStringsにエラーを切り分けます。

dictionary_test.go
func TestSearch(t *testing.T) {
    dictionary := map[string]string{"test": "this is just a test"}

    got := Search(dictionary, "test")
    want := "this is just a test"
		assertStrings(t, got, want)
}

func assertStrings(t *testing.T, got, want string) {
	t.Helper()
	if got != want {
		t.Errorf("got %q want %q given, %q", got, want, "test")
	}
}

続いて、dictionary構造体を用意し、そちらにSearchメソッドを紐づけます。
このようにDictionary型を用意することでどのようなドメインで使用するか理解しやすくなります。
これでテストがパスします。

dictionary.go
type Dictionary map[string]string

func (d Dictionary) Search(word string) string {
	return d[word]
}
dictionary_test.go
func TestSearch(t *testing.T) {
    dictionary := Dictionary{"test": "this is just a test"}

    got := dictionary.Search("test")
    want := "this is just a test"
		assertStrings(t, got, want)
}

最初にテストを書く

基本的な検索は非常に簡単に実装できましたが、辞書にない単語を指定するとどうなりますか?

実際には何も返されません。 プログラムは実行し続けることができるのでこれは良いですが、より良いアプローチがあります。 関数は、単語が辞書にないことを報告できます。 このように、ユーザーは単語が存在しないのか、それとも定義がないのか疑問に思うことはありません(これは、辞書にとってはあまり役に立たないように思われるかもしれません。ただし、他のユースケースで重要になる可能性があるシナリオです)。

dictionary_test.go
func TestSearch(t *testing.T) {
	dictionary := Dictionary{"test": "this is just a test"}
	t.Run("known word", func(t *testing.T) {

    got, _ := dictionary.Search("test")
    want := "this is just a test"
		assertStrings(t, got, want)
	})

	t.Run("unknown word", func(t *testing.T) {
		_, err := dictionary.Search("unknown")
		want := "could not find the word you were looking for"

		if err == nil {
				t.Fatal("expected to get an error.")
		}

		assertStrings(t, err.Error(), want)
	})
}

テストを試して実行すると./dictionary_test.go:18:10: assignment mismatch: 2 variables but 1 valuesと出力されます。

これは返却値として、値とエラーを期待しているが、関数ではそのような形になっていないことが原因です。

テストを実行して出力を確認するための最小限のコードを記述します

以下のように修正して、テストを実行します。
dictionary_test.go:19: expected to get an error.と出力されますが、ある意味キタいい通りの挙動です。エラーがnilを返しているためテストのケースで出力されている値だからです。

dictionary.go
type Dictionary map[string]string

func (d Dictionary) Search(word string) (string, error){
	return d[word], nil
}

成功させるのに十分なコードを書く

では、以下のように修正し、テストを成功させます。
マップの検索は返却値が存在しているかどうか(boolean)です。それを使用しています。

dictinary.go
package mapping

import "errors"

type Dictionary map[string]string

func (d Dictionary) Search(word string) (string, error){
	value, exists := d[word]
	if !exists {
		return "", errors.New("could not find the word you were looking for")
	}
	return value, nil
}

リファクタリング

簡単なリファクタリングを行います。
以下のように、ErrnotFountを変数に切り出してテストケースでも使用できるようにします。

dictionary.go
var ErrNotFound = errors.New("could not find the word you were looking for")

func (d Dictionary) Search(word string) (string, error) {
    definition, ok := d[word]
    if !ok {
        return "", ErrNotFound
    }

    return definition, nil
}

assertErrorに切り分け、引数に先ほど定義したErrNotFoundを渡します。

dictionary_test.go
t.Run("unknown word", func(t *testing.T) {
    _, got := dictionary.Search("unknown")

    assertError(t, got, ErrNotFound)
})
}

func assertError(t *testing.T, got, want error) {
    t.Helper()

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

最初にテストを書く

ここまでで辞書から単語を検索するようになりましたが、新しい単語を辞書に追加する方法はありません。
したがって、辞書に追加できるように実装していきます。

以下では空の辞書を用意して、Addで追加したのちに検索した場合、ヒットするかどうかによってテストを記述しています。

Add関数を実装していないためテストは当然失敗します。

dictionary_test.go

func TestAdd(t *testing.T) {
	dictionary := Dictionary{}
	dictionary.Add("test", "this is just a test")
	want := "this is just a test"
	got, err := dictionary.Search("test")

	if err != nil {
		t.Fatal("should find added word:", err)
	}

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

成功させるのに十分なコードを書く

簡単なコードなのでいきなり成功するコードを書きます。

dictionary.go
func (d Dictionary) Add(word, definition string){
	d[word] = definition
}

マップでは、以下のようにキーを指定して値を代入することでマップに追加できます。

.example
func main() {
    m := map[string]int{"apple": 100, "banana": 200}
    m["apple"] // 100
    m["banana"] = 300 // 300を200に代入
    m["orange"] = 400 // orange: 400が追加される
}

余談ですがマップは参照型なので値型の構造体のようにメモリアドレスを受け渡したりでリファレンスの必要がないです。以下他の場合もまとめておきます。

値型/参照型 説明
int 値型 整数型。コピーされる。
float64 値型 浮動小数点型。コピーされる。
bool 値型 ブール型。コピーされる。
string 値型 文字列型。コピーされる。
array 値型 配列。コピーされる。
struct 値型 構造体。コピーされる。
pointer 参照型 ポインタ。元の値を指し示す。
slice 参照型 スライス。配列への参照を保持する。
map 参照型 マップ。内部的にハッシュマップを保持する。
channel 参照型 チャネル。ゴルーチン間での通信に使われる。
function 参照型 関数。関数の定義を指し示す。
interface 参照型 インターフェース。特定のメソッドを持つ型を指し示す。

そして、参照型がもたらす落とし穴は、マップがnil値になる可能性があることです。
nilマップは読み取り時に空のマップのように動作しますが、nilマップに書き込もうとすると、ランタイムパニックが発生します。

したがって、空のマップ変数を初期化しないでください。

  • var m map[string]string

したがって、makeもしくは空のマップを用意して使用する必要があります。

  • var dictionary = make(map[string]string)
  • var dictionary = map[string]string{}

リファクタリング

そこまで多くリファクタリングはありませんが、assertDefinitionを定義して、独自のヘルパー関数を作成します。

dictionary_test.go
func TestAdd(t *testing.T) {
    dictionary := Dictionary{}
    word := "test"
    definition := "this is just a test"

    dictionary.Add(word, definition)

    assertDefinition(t, dictionary, word, definition)
}

func assertDefinition(t *testing.T, dictionary Dictionary, word, definition string) {
    t.Helper()

    got, err := dictionary.Search(word)
    if err != nil {
        t.Fatal("should find added word:", err)
    }

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

ただし、現時点では値がすでに存在する場合、マップはエラーをスローしません。
代わりに、先に進み、新しく提供された値で値を上書きします。これは実際には便利ですが、関数名が正確ではありません。 Addは既存の値を変更しません。辞書に新しい単語を追加するだけです。

最初にテストを書く

このテストでは、エラーを返すようにAddを変更しました。 これは、新しいエラー変数ErrWordExistsに対して検証しています。また、前のテストを変更して、nilエラーとassertError関数をチェックしました。

dictionary_test.go
func TestAdd(t *testing.T) {
    t.Run("new word", func(t *testing.T) {
        dictionary := Dictionary{}
        word := "test"
        definition := "this is just a test"

        err := dictionary.Add(word, definition)

        assertError(t, err, nil)
        assertDefinition(t, dictionary, word, definition)
    })

    t.Run("existing word", func(t *testing.T) {
        word := "test"
        definition := "this is just a test"
        dictionary := Dictionary{word: definition}
        err := dictionary.Add(word, "new test")

        assertError(t, err, ErrWordExists)
        assertDefinition(t, dictionary, word, definition)
    })
}
...
func assertError(t *testing.T, got, want error) {
    t.Helper()
    if got != want {
        t.Errorf("got %q want %q", got, want)
    }
    if got == nil {
        if want == nil {
            return
        }
        t.Fatal("expected to get an error.")
    }
}

当然失敗します。

./dictionary_test.go:30:13: dictionary.Add(word, definition) used as value
./dictionary_test.go:41:13: dictionary.Add(word, "new test") used as value

テストを実行して出力を確認するための最小限のコードを記述します

dictionary.go
var (
    ErrNotFound   = errors.New("could not find the word you were looking for")
    ErrWordExists = errors.New("cannot add word because it already exists")
)

func (d Dictionary) Add(word, definition string) error {
    d[word] = definition
    return nil
}

これで、さらに2つのエラーが発生します。まだ値を変更しており、 nilエラーを返しています。

dictionary_test.go:43: got error '%!q(<nil>)' want 'cannot add word because it already exists'
dictionary_test.go:44: got 'new test' want 'this is just a test'

成功させるのに十分なコードを書く

次のように書くことでテストはパスします。
Searchメソッドでwordを検索して、そのエラーをswitchで確認します。
errorがない場合は単語が存在し、errorがある場合は単語が存在しないということなので以下の通りとなります。

dictionary.go
func (d Dictionary) Add(word, definition string) error {
	_, err := d.Search(word)

	switch err{
	case ErrNotFound:
		d[word] = definition
	case nil:
		return ErrWordExists
	default:
		return err
	}

	return nil
}

個人的には今回のケースなら以下の方が簡潔なのでいいかなと思いました。

dictionary.go
func (d Dictionary) Add(word, definition string) error {
	_, exists := d[word]

	if exists {
		return ErrWordExists
	}

	d[word] = definition
	return nil
}

ともあれテストはこれでパスします。

リファクタリング

リファクタリングするものはあまりありませんが、エラーの使用が増えるにつれて、いくつかの変更を加えることができます。

これはこれまでに何度かでてきていますが、DictionaryErr型を定義し、それにError()を紐づけることで出力をカスタマイズする方法です。

dictionary.go
const (
    ErrNotFound   = DictionaryErr("could not find the word you were looking for")
    ErrWordExists = DictionaryErr("cannot add word because it already exists")
)

type DictionaryErr string

func (e DictionaryErr) Error() string {
    return string(e)
}

次に、単語の定義をUpdateする関数を作成しましょう

最初にテストを書く

UpdateAddとほぼ同じです。しかし、Addの場合は存在していると失敗する点が異なる点です。
keyで検索して存在しなければエラー、存在すればその値を更新する方針で良さそうです。

以下のようにテストを書き、失敗を確認します。

dictionary_test.go
func TestUpdate(t *testing.T) {
    word := "test"
    definition := "this is just a test"
    dictionary := Dictionary{word: definition}
    newDefinition := "new definition"

    dictionary.Update(word, newDefinition)

    assertDefinition(t, dictionary, word, newDefinition)
}

テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認する

dictionary.go
func (d Dictionary) Update(word, newDefinition string) {
} 

最低限のコードを書き、テストを実行すると、以下の通り失敗します。
dictionary_test.go:55: got "this is just a test" want "new definition"

成功させるのに十分なコードを書く

まずはすでに存在するかどうかは無視して、更新するコードを書きます。
これで現在のテストはすでに存在しているキーのためパスします。ただし、Addと同じ問題が発生しました。新しい単語を渡すと、 Updateはそれを辞書に追加します。

dictinary.go
func (d Dictionary) Update(word, definition string) {
	d[word] = definition
} 

そこでテストを修正します。

存在しているかどうかチェックするテスト

リストにワードが存在している場合と存在していない場合に分けてテストを書きます。
存在している場合は、その値を更新し、存在しない場合は、エラーが発生することを期待しています。

dictionary_test.go
func TestUpdate(t *testing.T){
	t.Run("existing word", func(t *testing.T) {
		word := "test"
		definition := "this is just a test"
		dictionary := Dictionary{word: definition}
		newDefinition := "new definition"
		err := dictionary.Update(word, newDefinition)
	
		assertError(t, err, nil)
		assertDefinition(t, dictionary, word, newDefinition)
	})

	t.Run("new word", func(t *testing.T) {
		word := "test"
		definition := "this is just a test"
		dictionary := Dictionary{}

		err := dictionary.Update(word, definition)

		assertError(t, err, ErrWordDoesNotExist)
	})
}

しかし、この段階ではテストに失敗します。

./dictionary_test.go:53:16: dictionary.Update(word, "new test") used as value
./dictionary_test.go:64:16: dictionary.Update(word, definition) used as value
./dictionary_test.go:66:23: undefined: ErrWordDoesNotExist

テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します

以下の2点を変更します。

  • ErrWordDoesNotExist = DictionaryErr("cannot update word because it does not exist")という変数を追加
  • 関数でエラーを返却するように修正
dictinary.go
var ErrWordDoesNotExist = errors.New("cannot update word because it does not exist")
...
func (d Dictionary) Update(word, definition string) error {
	d[word] = definition
	return nil
} 

これでテストを実行すると、以下の通りテストは実行され、期待しているエラーではないため失敗します。
そこで、存在しない場合はエラーを起こすように修正します。

dictionary_test.go:67: got %!q(<nil>) want "cannot update word because it does not exist"
dictionary_test.go:67: expected to get an error.

成功させるのに十分なコードを書く

これはもはやAddとほとんど同じです。

dictionary.go
func (d Dictionary) Update(word, definition string) error {
	_, err := d.Search(word)

	switch err {
		case ErrNotFound:
			return ErrWordDoesNotExist
		case nil:
			d[word] = definition
		default:
			return err
	}

	return nil
} 

次に、辞書の単語を削除(Delete)する関数を作成しましょう。
これまでにマップに追加したり、更新したりしてきましたが、最後に削除できるようにします。
削除はdelete(m, "Key")のようにマップとキーを指定すれば削除可能です。

最初にテストを書く

このテストではDictionaryを作成し、それを削除したのちに本当に削除できているかどうかSearchしています。テストは当然パスしないので次に関数を作成します。

dictionary_test.go
func TestDelete(t *testing.T){
	word := "test"
	definition := "this is just a test"
	dictionary := Dictionary{word: definition}

	dictionary.Delete(word)

	_, err := dictionary.Search(word)
	if err != ErrNotFound {
			t.Errorf("Expected %q to be deleted", word)
	}
}

テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します

とりあえず以下のように最小限のコードを書きます。

func (d Dictionary) Delete(word string) {}

テストはここでも当然dictionary_test.go:78: Expected 'test' to be deletedと失敗します。

成功させるのに十分なコードを書く

前述した通りdelete(map, key)で削除できます。

dictionary.go
func (d Dictionary) Delete(word string) {
    delete(d, word)
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0