맵
맵은 사전과 비슷한 방식으로 항목을 저장할 수 있어서,
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
이라는 키워드로 시작하고 두 개의 타입이 있어야한다. 첫번째 타입은 키 타입으로 []
안에 쓰여 있다. 두번째는 값 타입으로, []
바로 다음에 온다.키 타입은 특별하다. 키 타입에는 오직 비교 가능한 타입만이 올 수 있는데 왜냐하면 두개의 키가 동일한지 판별할 수 없다면 올바른 값을 가져왔는지 확신할 수 있는 방법이 없기 때문이다. 언어 명세에 비교 가능한 타입이 자세하게 설명되어 있다.
반면에 값 타입으로 무엇이든 원하는 값이 가능하다. 심지어 또다른 맵도 가능하다.
테스트의 나머지는 친숙할 것이다.
go test
를 실행하면 컴파일러는 ./dictionary_test.go:8:9: undefined: Search
와 함께 실패할 것이다.In
dictionary.go
package main
func Search(dictionary map[string]string, word string) string {
return ""
}
이번에는 테스트가 명확한 에러 메시지와 함께 실패할 것이다.
dictionary_test.go:12: got '' want 'this is just a test' given, 'test'
.func Search(dictionary map[string]string, word string) string {
return dictionary[word]
}
맵에서 값을 가져오는 것은
map[key]
배열에서 값을 가져오는 것과 동일하다.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.TB, got, want string) {
t.Helper()
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
assertStrings
헬퍼를 만듦으로써 구현이 보다 일반적이게 되도록 만들었다.map에 대한 새로운 타입을 만들고
Search
함수를 만듦으로써 위에서 작성한 사전의 사용성을 개선한다.In
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
타입을 도입했는데 아직 선언하지 않았다. 그리고 Dictionary
인스턴스의 Search
함수를 호출하였다.assertStrings
를 변경할 필요는 없다.dictionary.go
에서:type Dictionary map[string]string
func (d Dictionary) Search(word string) string {
return d[word]
}
여기서
Dictionary
타입을 생성했는데, map
을 감싸는 얇은 래퍼로 동작합니다. 새로 정의한 커스텀 타입과 함께, Search
함수를 생성할 수 있다.기본 검색은 구현하기 매우 쉬웠다. 그러나 만약 딕셔너리에 없는 단어를 검색한다면 어떻게 될까?
실제로 아무 것도 가져올 수 없다. 이래도 프로그램이 계속 동작하게하기 때문에 괜찮지만 더 나은 방법이 있다.
Search
함수는 단어가 사전에 존재하지 않는다고 알려줄 수 있다. 이 방법으로 사용자가 단어가 존재하지 않는건지 아니면 단지 정의가 없는건지 궁금해하지 않게 된다 (이런 사전은 유용하지 않아 보인다. 그렇지만 다른 사례에서 키가 될 시나리오이다).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)
})
}
Go에서 이러한 시나리오를 다루는 방법은 두번째 인자인
Error
타입을 활용하는 것이다.Error
는 .Error()
메소드를 통해 문자열로 변환될 수 있다. 이 문자열은 assertion에 넘겨주는 대상이다. 또한 if
조건문을 통해 assertStrings
를 보호함으로써 error
가 nil
일 때 .Error()
를 호출하지 않게끔 보장한다.위 코드는 컴파일 되지 않다:
./dictionary_test.go:18:10: assignment mismatch: 2 variables but 1 values
func (d Dictionary) Search(word string) (string, error) {
return d[word], nil
}
이번에 작성한 테스트는 보다 명확한 에러 메시지와 함께 실패할 것이다.
dictionary_test.go:22: expected to get an error.
func (d Dictionary) Search(word string) (string, error) {
definition, ok := d[word]
if !ok {
return "", errors.New("could not find the word you were looking for")
}
return definition, nil
}
테스트를 통과하기 위해서, 맵 탐색의 흥미로운 특성을 사용했다. 맵은 두 개의 값을 반환한다. 두번째 값은 boolean으로 키를 찾는데 성공했는지를 가리 킨다.
이 성질을 이용해서 단어가 존재하지 않는 것과 단어에 정의가 없는 것을 구분할 수 있다.
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
}
Search
함수의 매직 에러를 별개의 변수로 뽑아냄으로써 이 에러를 제거할 수 있다. 이것은 더 나은 테스트를 만들 수 있도록 한다.t.Run("unknown word", func(t *testing.T) {
_, got := dictionary.Search("unknown")
assertError(t, got, ErrNotFound)
})
}
func assertError(t testing.TB, got, want error) {
t.Helper()
if got != want {
t.Errorf("got error %q want %q", got, want)
}
}
새 헬퍼를 만든 덕에 테스트가 더 간결해질 수 있었다.
ErrNotFound
변수를 사용함으로써 에러 문자열이 나중에 바뀌더라도 테스트가 실패하지 않게했다.사전을 검색하는 훌륭한 방법을 갖추었다. 그러나 우리의 사전에 새 단어를 추가하는 방법이 없다.
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)
}
}
이 테스트는
Search
함수를 활용하여 사전 검사를 보다 쉽게했다.In
dictionary.go
func (d Dictionary) Add(word, definition string) {
}
테스트는 이제 실패할 것이다
dictionary_test.go:31: should find added word: could not find the word you were looking for
func (d Dictionary) Add(word, definition string) {
d[word] = definition
}
맵에 추가하는 것은 배열과 유사하다. 키를 명시하고 값을 같게하면 된다.
맵의 흥미로운 특성은 그것의 주소를 전달(예컨데
&myMap
)하지 않고서도 수정할 수 있다는 것이다.맵에 함수/메소드를 전달하게되면 실제로 맵을 복사하지만 단지 포인터 부분만 해당한다. 데이터를 갖고 있는 하부 자료 구조는 복사하지 않는다.
맵에 관해 유의할 점은
nil
값이 가능하다는 점이다. 읽기 작업을 수행할 때 nil
맵은 빈 맵과 동일하게 동작하지만 nil
맵에 쓰기 작업을 시도한다면 이는 런타임 패닉을 일으키는 원인이 된다. 맵에 관해서는 여기서 더 알아볼 수 있다.따라서, 절대로 빈 맵을 초기화 해서는 안 된다:
var m map[string]string
대신에, 위에서 해본 것처럼 빈 맵을 초기화 할 수 있는데 아니면 맵을 새로 생성하는
make
키워드를 사용할 수 있다:var dictionary = map[string]string{}
// OR
var dictionary = make(map[string]string)
두 방법은 빈
hash map
을 생성하고 dictionary
가 이를 가리키게 한다. 이것은 절대로 런타임 패닉을 발생하지 않도록 보장하는 방법이다.