マップ
Maps
配列とスライスでは、値を順番に格納する方法を見ました。 では、keyでアイテムを保存し、すばやく検索する方法を見てみましょう。
マップを使用すると、辞書と同じようにアイテムを保存できます。 keyは単語、valueは定義と考えることができます。 そして、独自の辞書を構築するよりも、マップについて学ぶより良い方法は何でしょうか?
まず、辞書に定義された単語がすでにあると仮定すると、単語を検索すると、その単語の定義が返されます。
最初にテストを書く
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キーワードで始まり、2つのタイプが必要です。 1つはキーのタイプで、[]内に記述されます。 2番目は値のタイプで、 []の直後に続きます。
キーのタイプは特別です。 2つのキーが等しいかどうかを判別できないと、正しい値が取得されていることを確認する方法がないため、比較可能な型にしかできません。比較可能な型については、言語仕様で詳しく説明しています。
一方、値タイプは任意のタイプにすることができます。別のマップにすることもできます。
このテストの他のすべてはよく知っている必要があります。
テストを実行してみます
go testを実行すると、コンパイラーは「./dictionary_test.go:8:9: undefined: Search」で失敗します。
テストを実行して出力を確認するための最小限のコードを記述します
dictionary.go
テストは clearエラーメッセージ で失敗するはずです。
dictionary_test.go:12: got '' want 'this is just a test' given, 'test'.
成功させるのに十分なコードを書く
マップから値を取得することは、配列 map[key]から値を取得することと同じです。
リファクタリング♪
実装をより一般的なものにするために、 assertStringsヘルパーを作成することにしました。
カスタムタイプを使用する
マップの周りに新しいタイプを作成し、 Searchをメソッドにすることで、辞書の使用法を改善できます。
dictionary_test.go:
まだ定義していない Dictionaryタイプを使い始めました。次に、 DictionaryインスタンスでSearchを呼び出します。
assertStringsを変更する必要はありませんでした。
dictionary.go:
ここでは、 mapの薄いラッパーとして機能するDictionaryタイプを作成しました。 カスタムタイプが定義されたら、Searchメソッドを作成できます。
最初にテストを書く
基本的な検索は非常に簡単に実装できましたが、辞書にない単語を指定するとどうなりますか?
実際には何も返されません。 プログラムは実行し続けることができるのでこれは良いですが、より良いアプローチがあります。 関数は、単語が辞書にないことを報告できます。 このように、ユーザーは単語が存在しないのか、それとも定義がないのか疑問に思うことはありません(これは、辞書にとってはあまり役に立たないように思われるかもしれません。ただし、他のユースケースで重要になる可能性があるシナリオです)。
Goでこのシナリオを処理する方法は、 Errorタイプである2番目の引数を返すことです。
Errorは、.Error()メソッドで文字列に変換できます。 これは、アサーションに渡すときに行います。 また、nilで.Error()を呼び出さないように、assertStringsをifで保護しています。
テストを試して実行する
これはコンパイルされません
テストを実行して出力を確認するための最小限のコードを記述します
テストは失敗し、より明確なエラーメッセージが表示されます。
dictionary_test.go:22: expected to get an error.
成功させるのに十分なコードを書く
このパスを作成するために、マップルックアップの興味深いプロパティを使用しています。 2つの値を返すことができます。 2番目の値は、キーが正常に検出されたかどうかを示すブール値です。
このプロパティにより、存在しない単語と定義がない単語を区別できます。
リファクタリング♪
変数に抽出することで、Search関数の魔法のエラーを取り除くことができます。 これにより、より良いテストを行うことができます。
新しいヘルパーを作成することで、テストを簡素化し、 ErrNotFound変数の使用を開始できるため、将来エラーテキストを変更してもテストが失敗しません。
Write the test first
辞書を検索するには素晴らしい方法があります。ただし、新しい単語を辞書に追加する方法はありません。
このテストでは、 Search関数を使用して、辞書の検証を少し簡単にします。
テストを実行して出力を確認するための最小限のコードを記述します
dictionary.go
これでテストは失敗するはずです
成功させるのに十分なコードを書く
マップへの追加も配列に似ています。キーを指定して、値に設定するだけです。
参照型
マップの興味深い特性は、マップをポインタとして渡さなくても変更できることです。これは、 mapが参照型であるためです。つまり、ポインタのように、基礎となるデータ構造への参照を保持します。 基本的なデータ構造はhash tablesまたはhash mapであり、hash tablesの詳細についてはこちらを参照してください。
マップがどれほど大きくても、コピーは1つしかないので、マップは参照として非常に適しています。
参照型がもたらす落とし穴は、マップがnil値になる可能性があることです。 nilマップは読み取り時に空のマップのように動作しますが、nilマップに書き込もうとすると、ランタイムパニックが発生します。 マップの詳細については、こちらをご覧ください。
したがって、空のマップ変数を初期化しないでください。
代わりに、上記のように空のマップを初期化するか、makeキーワードを使用してマップを作成できます。
どちらのアプローチでも空のhash mapを作成し、dictionaryを指し示します。これにより、ランタイムパニックが発生することはありません。
リファクタリング♪
私たちの実装ではリファクタリングするものは多くありませんが、テストでは少し単純化を使用できます。
単語と定義の変数を作成し、定義アサーションを独自のヘルパー関数に移動しました。
Addは見栄えがしました。ただし、追加しようとしている値が既に存在する場合に何が起こるかは考慮しませんでした。
値がすでに存在する場合、マップはエラーをスローしません。 代わりに、先に進み、新しく提供された値で値を上書きします。これは実際には便利ですが、関数名が正確ではありません。 Addは既存の値を変更しません。辞書に新しい単語を追加するだけです。
最初にテストを書く
このテストでは、エラーを返すようにAddを変更しました。 これは、新しいエラー変数ErrWordExistsに対して検証しています。また、前のテストを変更して、nilエラーとassertError関数をチェックしました。
テストを実行してみます
Addの値を返さないため、コンパイラは失敗します。
テストを実行して出力を確認するための最小限のコードを記述します
dictionary.go
これで、さらに2つのエラーが発生します。まだ値を変更しており、 nilエラーを返しています。
成功させるのに十分なコードを書く
ここでは、エラーを照合するために switchステートメントを使用しています。このようなswitchがあると、SearchがErrNotFound以外のエラーを返す場合に備えて、追加の安全策が提供されます。
リファクタリング♪
リファクタリングするものはあまりありませんが、エラーの使用が増えるにつれて、いくつかの変更を加えることができます。
エラーを一定にしました。これには、 errorインターフェースを実装する独自の DictionaryErrタイプを作成する必要がありました。詳細については、Dave Cheneyによるこの優れた記事を参照してください。 簡単に言うと、エラーが再利用可能で不変になります。
次に、単語の定義をUpdateする関数を作成しましょう。
最初にテストを書く
UpdateはAddと非常に密接に関連しており、次の実装になります。
テストを試して実行する
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認する
このようなエラーに対処する方法はすでに知っています。関数を定義する必要があります。
これを実行すると、単語の定義を変更する必要があることがわかります。
成功させるのに十分なコードを書く
Addで問題を修正したときに、これを行う方法はすでに見ました。 それでは、Addに本当に似たものを実装しましょう。
これは単純な変更だったので、これに必要なリファクタリングはありません。ただし、Addと同じ問題が発生しました。 新しい単語を渡すと、 Updateはそれを辞書に追加します。
最初にテストを書く
単語が存在しない場合のエラータイプをさらに追加しました。また、Updateを変更してerror値を返すようにしました。
テストを試して実行する
今回は3つのエラーが発生しますが、対処方法はわかっています。
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
独自のエラータイプを追加し、nilエラーを返しています。
これらの変更により、非常に明確なエラーが発生します。
成功させるのに十分なコードを書く
この関数は、dictionaryを更新したときとエラーを返したときを除いて、Addとほとんど同じように見えます。
更新(Update)の新しいエラーの宣言に関する注意
ErrNotFoundを再利用して、新しいエラーを追加することはできません。ただし、更新が失敗したときに正確なエラーを表示する方がよい場合がよくあります。
特定のエラーがあると、何が問題だったかに関する詳細情報が得られます。以下はWebアプリの例です。
ErrNotFoundが発生したときにユーザーをリダイレクトできますが、ErrWordDoesNotExistが発生したときにエラーメッセージを表示できます。
次に、辞書の単語を削除(Delete)する関数を作成しましょう。
最初にテストを書く
このテストでは、単語を含むDictionaryを作成し、単語が削除されているかどうかを確認します。
Try to run the test
go testを実行すると、次のようになります。
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
これを追加した後、テストは単語を削除しないことを通知します。
成功させるのに十分なコードを書く
Goには、マップで機能する組み込み関数deleteがあります。 2つの引数を取ります。1つ目はマップで、2つ目は削除するキーです。
delete関数は何も返さず、同じ概念に基づいてDeleteメソッドを作成しました。存在しない値を削除しても効果がないため、UpdateやAddメソッドとは異なり、APIを複雑にしてエラーを発生させる必要はありません。
まとめ
このセクションでは、多くのことを取り上げました。辞書用に完全なCRUD(作成、読み取り、更新、削除)APIを作成しました。プロセス全体を通じて、次の方法を学びました。
マップを作成する
マップ内のアイテムを検索
マップに新しいアイテムを追加する
マップのアイテムを更新する
マップからアイテムを削除する
エラーの詳細
定数であるエラーを作成する方法
エラーラッパーを書く
最終更新
役に立ちましたか?