![[Learn Go with Tests] 맵](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbFxsIp%2FbtsIKTjb3GK%2FAAAAAAAAAAAAAAAAAAAAAFL6jTL6lqWhOiP9eCJyfzLBcj4lo5-W3j_UK4wXmHTm%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1756652399%26allow_ip%3D%26allow_referer%3D%26signature%3D2aWtEke7KNE2BJ3oNWx0W4FYX8k%253D)
이전의 배열과 슬라이스에서는 값을 순서대로 저장하는 방법에 대해 학습했다. 이번에는 항목을 key에 따라 저장하고, 저장한 key를 찾는 방법에 대해 알아보도록 하자.
⇒ Key-Value가 있던 python의 Dictionary와 유사하다!
테스트 코드 작성하기
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이라는 키워드로 시작하고 두개의 타입이 필요하다.
- 첫번째 타입 → [] 안에 들어있다
- 두번째 타입 → [] 뒤에 위치한다.
- 키 타입 → 오직 비교가능한 타입만이 올 수 있다.
- 같은 타입이면 올바른 값을 가져왔는지 파악할 수 없으므로 이를 유의하자
- 반면, 값타입은 모든 값이 가능하다.
서비스 코드 작성하기
func Search(dictionary map[string]string, word string) string {
return dictionary[word]
}
리팩토링 하기
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)
}
}
assertString을 만듦으로써 구현이 일반적이도록 수정하였다.
커스텀 타입 사용하기
map에 대한 새로운 타입을 만들고, Search 함수를 만듦으로써 사전의 사용성을 개선할 수 있다.
테스트 코드
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의 Searcy 함수를 메서드로 추가한다.
서비스 코드
type Dictionary map[string]string
func (d Dictionary) Search(word string) string {
return d[word]
}
위와 같이 간결하게 바꾸어 줄 수 있다.
사전에 없는 단어 검색
사전에 없는 단어를 검색하면 아무것도 가져올 수 없다. 이때 존재하지 않는 단어임을 알려주는 방향으로 코드를 변경해보도록 하자.
테스트 코드 작성하기
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)
})
}
- Error는 .Error() 메소드를 통해서 문자열 변환이 가능하다.
- 문자열 → assertion에 넘겨주는 대상이 된다.
- if문을 통해 assertion에 넘겨주기 전 error가 nil인 경우 .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
}
- Map은 두개의 반환 값을 사용한다는 특성을 활용하여 두번째 값은 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
}
에러를 이전과 같이 별개의 변수로 사용하면서 에러 제거가 가능하다.
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)
}
}
테스트 코드의 경우 새로운 헬퍼를 통해 더 간결해진 에러 확인이 가능하다.
사전에 새 단어 추가하기
테스트 코드 작성하기
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 함수를 통해 사전 검사를 보다 쉽게 한다.
서비스 코드 작성하기
func (d Dictionary) Add(word, definition string) {
d[word] = definition
}
- 맵에 데이터를 추가할 때에는 배열과 유사하게 키를 명시하고 값을 대입하면 된다.
맵의 특성
- &map 과 같이 사용하지 않아도 값을 수정하는 것이 가능하다.
- 함수에 map을 전달 → 포인터 부분을 전달하는 것이고, 데이터와 같은 자료구조는 복사하는 것이 아니다.
- 유의사항 → nil 값이 가능하므로, 빈 맵과 동일하게 작동하지만 nil 맵에 쓰기 작업을 시도하면 패닉이 일어난다.
- 빈 맵에 대한 키 값을 조회하려고 하면 값이 조회되지 않아 오류가 발생하는 것이다.
⇒ 절대 빈 맵을 초기화 하면 안된다 !!
빈 맵을 초기화 하는 방법
var dictionary = map[string]string{}
// OR
var dictionary = make(map[string]string)
- 위와 같은 두가지 방법은 비어있는 hash map을 생성하고 dictionary가 이를 가리키게 하는 방법으로, 런타임 패닉이 발생하지 않는다.
⇒ 비어있는 값에 직접적으로 접근하려 하면 패닉이 발생, 위와 같이 포인터로 가리키게 하는 방법은 패닉 발생 X
리팩터링 하기
테스트를 간결하게 리팩터링 해보자
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.TB, 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)
}
}
⇒ 단어와 정의를 위한 변수를 만들고 검사 로직을 별도 헬퍼 함수로 빼낸다.
이미 존재하는 값을 추가할 때의 경우 고려
테스트 코드 작성하기
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)
})
}
- Add 함수가 Error를 반환하도록 수정하였다. ErrWordExist를 검증하는 방식으로 고려하도록 하자.
- 이전의 nil 에러 검사와 assertError 또한 마찬가지로 구성된다.
서비스 코드 수정하기
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
}
switch 구문을 활용하여 에러를 매칭해보도록 하자.
리팩터링 하기
에러 활용도가 커져감에 따라 에러를 간결하게 볼 수 있도록 리팩토링하고자 한다.
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)
}
에러를 다음과 같이 상수로 만들 수 있다.
- error 인터페이스 구현 → DictionaryErr 타입을 만들어 위와 같이 Error 인터페이스를 구현해 커스텀 에러를 만들 수 있다.
단어의 정의 업데이트 하기
테스트 코드 작성하기
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)
}
add 함수와 밀접한 관련이 있는 코드를 볼 수 있다.
서비스 코드 작성하기
func (d Dictionary) Update(word, definition string){
d[word] = definition
}
간단하게 이렇게 구현할 수 있지만, 이러면 add 함수와 똑같은 문제가 있다. 사전에 없는 단어를 전달하면 update는 오히려 이를 추가할 수 있다.
사전에 없는 단어 Update에 전달하면?
테스트 코드 작성하기
func TestUpdate(t *testing.T) {
t.Run("existing word", func(t *testing.T) {
word := "test"
definition := "this is just a test"
newDefinition := "new definition"
dictionary := Dictionary{word: 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)
})
}
이렇게 단어가 존재하지 않는 경우에 대한 에러를 추가할 수 있다.
서비스 코드 변경하기
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
}
Add 함수와 유사하지만 Error 종류에 따라 업데이트 하는 경우가 바뀐다.
사전에서 단어 삭제하기
테스트 코드 작성하기
func TestDelete(t *testing.T) {
word := "test"
dictionary := Dictionary{word: "test definition"}
dictionary.Delete(word)
_, err := dictionary.Search(word)
if err != ErrNotFound {
t.Errorf("Expected %q to be deleted", word)
}
}
단어와 함께 Dictionary를 생성하고 해당 단어가 사라졌는지 확인할 수 있다.
서비스 코드 작성하기
func (d Dictionary) Delete(word string) {
delete(d, word)
}
- go 에서는 delete라는 내장함수를 통해 map과 삭제할 키를 사용하여 map의 key-value를 삭제하는 것이 가능하다.
- delete 함수는 아무것도 반환하지 않는다.
- 이에 기초하여, 존재하지 않는 값을 삭제하면 아무런 영향이 없다.
'Language > Go' 카테고리의 다른 글
[Learn Go with Tests] 의존성 주입 (13) | 2024.07.24 |
---|---|
[Learn Go with Tests] 포인터 & 에러 (1) | 2024.07.24 |
[Learn Go with Tests] 구조체, 메서드 & 인터페이스 (0) | 2024.07.18 |
[Learn Go with Tests] 슬라이스 및 배열 (0) | 2024.07.18 |
[Learn Go with Tests] Iteration (0) | 2024.07.18 |
보안 전공 개발자지만 대학로에서 살고 싶어요
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!