Trong arrays & slices, bạn thấy cách lưu trữ các giá trị theo thứ tự. Giờ chúng ta sẽ xem xét cách lưu trữ các mục theo key và tra cứu nhanh.
Maps cho phép bạn lưu trữ các mục theo cách tương tự như từ điển. Bạn có thể nghĩ key là từ và value là định nghĩa. Và cách nào tốt hơn để học về Maps hơn là xây dựng từ điển của riêng chúng ta?
Đầu tiên, giả sử chúng ta đã có một số từ với định nghĩa trong từ điển, nếu tìm kiếm một từ, nó sẽ trả về định nghĩa của từ đó.
Viết test trước tiên
Trong dictionary_test.go
packagemainimport"testing"funcTestSearch(t*testing.T){dictionary:=map[string]string{"test":"this is just a test"}got:=Search(dictionary,"test")want:="this is just a test"ifgot!=want{t.Errorf("got %q want %q given, %q",got,want,"test")}}
Khai báo Map khá giống với array. Ngoại trừ, nó bắt đầu bằng từ khóa map và yêu cầu hai kiểu. Kiểu đầu tiên là kiểu key, được viết trong []. Kiểu thứ hai là kiểu value, ngay sau [].
Kiểu key là đặc biệt. Nó chỉ có thể là kiểu comparable vì nếu không có khả năng so sánh 2 keys bằng nhau, chúng ta không có cách nào đảm bảo chúng ta đang lấy đúng value. Các kiểu comparable được giải thích kỹ trong language spec.
Kiểu value, mặt khác, có thể là bất kỳ kiểu nào. Thậm chí có thể là một map khác.
Mọi thứ khác trong test này đã quen thuộc.
Thử chạy test
Bằng cách chạy go test, compiler sẽ thất bại với ./dictionary_test.go:8:9: undefined: Search.
Viết lượng code tối thiểu để chạy test và kiểm tra kết quả lỗi
Trong dictionary.go
Test của bạn giờ sẽ thất bại với thông báo lỗi rõ ràng
dictionary_test.go:12: got '' want 'this is just a test' given, 'test'.
Viết đủ code để test chạy thành công
Lấy giá trị từ Map giống như lấy từ Array map[key].
Refactor
Tôi quyết định tạo helper assertStrings để khái quát hóa implementation hơn.
Dùng kiểu tùy chỉnh
Chúng ta có thể cải thiện cách dùng từ điển bằng cách tạo kiểu mới xung quanh map và biến Search thành method.
Trong dictionary_test.go:
Chúng ta bắt đầu dùng kiểu Dictionary, mà chúng ta chưa định nghĩa. Sau đó gọi Search trên instance Dictionary.
Chúng ta không cần thay đổi assertStrings.
Trong dictionary.go:
Ở đây chúng ta tạo kiểu Dictionary hoạt động như wrapper mỏng quanh map. Với kiểu tùy chỉnh được định nghĩa, chúng ta có thể tạo method Search.
Viết test trước tiên
Tìm kiếm cơ bản rất dễ implement, nhưng điều gì sẽ xảy ra nếu chúng ta cung cấp từ không có trong từ điển?
Thực ra chúng ta không nhận được gì. Điều này tốt vì chương trình có thể tiếp tục chạy, nhưng có cách tiếp cận tốt hơn. Hàm có thể báo cáo từ không có trong từ điển. Như vậy người dùng không phải tự hỏi từ đó không tồn tại hay chỉ là không có định nghĩa (điều này không có vẻ hữu ích cho từ điển, nhưng là tình huống có thể quan trọng trong các use case khác).
Cách xử lý tình huống này trong Go là trả về đối số thứ hai kiểu Error.
Chú ý rằng như đã thấy trong phần pointers và error, để assert thông báo lỗi, trước tiên chúng ta kiểm tra lỗi không phải nil rồi dùng method .Error() để lấy string mà chúng ta có thể truyền vào assertion.
Thử chạy test
Code này không biên dịch được
Viết lượng code tối thiểu để chạy test và kiểm tra kết quả lỗi
Test của bạn sẽ thất bại với thông báo lỗi rõ ràng hơn.
dictionary_test.go:22: expected to get an error.
Viết đủ code để test chạy thành công
Để test pass, chúng ta dùng thuộc tính thú vị của map lookup. Nó có thể trả về 2 giá trị. Giá trị thứ hai là boolean báo hiệu key có được tìm thấy không.
Thuộc tính này cho phép chúng ta phân biệt giữa từ không tồn tại và từ không có định nghĩa.
Refactor
Chúng ta có thể loại bỏ magic error trong hàm Search bằng cách trích xuất nó thành biến. Điều này cũng cho phép test tốt hơn.
Bằng cách tạo helper mới, chúng ta đơn giản hóa test và bắt đầu dùng biến ErrNotFound để test không fail nếu chúng ta thay đổi văn bản lỗi sau này.
Viết test trước tiên
Chúng ta có cách tốt để tìm kiếm trong từ điển. Tuy nhiên, chúng ta không có cách nào thêm từ mới.
Trong test này, chúng ta dùng hàm Search để validate từ điển dễ hơn.
Viết lượng code tối thiểu để chạy test và kiểm tra kết quả lỗi
Trong dictionary.go
Test của bạn sẽ thất bại
Viết đủ code để test chạy thành công
Thêm vào map cũng giống như array. Chỉ cần chỉ định key và đặt nó bằng value.
Pointers, copies, v.v.
Một thuộc tính thú vị của maps là bạn có thể sửa đổi chúng mà không cần truyền địa chỉ (ví dụ &myMap)
Điều này có thể khiến chúng cảm thấy như "kiểu tham chiếu", nhưng như Dave Cheney mô tả, chúng không phải vậy.
Giá trị map là pointer đến cấu trúc runtime.hmap.
Vì vậy khi bạn truyền map vào hàm/method, bạn đang sao chép nó, nhưng chỉ phần pointer, không phải cấu trúc dữ liệu bên dưới chứa data.
Điều đáng chú ý với maps là chúng có thể là giá trị nil. Map nil hoạt động như map rỗng khi đọc, nhưng cố ghi vào map nil sẽ gây ra runtime panic. Bạn có thể đọc thêm về maps tại đây.
Do đó, bạn không bao giờ nên khởi tạo biến map nil:
Thay vào đó, có thể khởi tạo map rỗng hoặc dùng từ khóa make để tạo map:
Cả hai cách đều tạo hash map rỗng và trỏ dictionary vào nó, đảm bảo bạn không bao giờ gặp runtime panic.
Refactor
Không có nhiều thứ cần refactor trong implementation nhưng test có thể đơn giản hóa một chút.
Chúng ta tạo biến cho word và definition, và chuyển assertion definition sang hàm helper riêng.
Add của chúng ta trông tốt. Ngoại trừ, chúng ta không xem xét điều gì xảy ra khi value chúng ta cố thêm đã tồn tại!
Map không throw lỗi nếu value đã tồn tại. Thay vào đó, chúng sẽ ghi đè value bằng value mới được cung cấp. Điều này có thể tiện lợi trong thực tế, nhưng khiến tên hàm kém chính xác. Add không nên sửa đổi các giá trị đã có. Nó chỉ nên thêm từ mới vào từ điển.
Viết test trước tiên
Trong test này, chúng ta sửa đổi Add để trả về lỗi, mà chúng ta validate dựa trên biến lỗi mới, ErrWordExists. Chúng ta cũng sửa test trước để kiểm tra lỗi nil.
Thử chạy test
Compiler sẽ thất bại vì chúng ta không trả về value cho Add.
Viết lượng code tối thiểu để chạy test và kiểm tra kết quả lỗi
Trong dictionary.go
Bây giờ chúng ta nhận được thêm hai lỗi. Chúng ta vẫn đang sửa đổi value và trả về lỗi nil.
Viết đủ code để test chạy thành công
Ở đây chúng ta dùng câu lệnh switch để match trên lỗi. Có switch như này cung cấp mạng lưới an toàn thêm, phòng trường hợp Search trả về lỗi khác ngoài ErrNotFound.
Refactor
Chúng ta không có nhiều thứ cần refactor, nhưng khi error usage phát triển, chúng ta có thể thực hiện một vài sửa đổi.
Chúng ta tạo errors là constants; điều này yêu cầu tạo kiểu DictionaryErr riêng implement interface error. Bạn có thể đọc thêm trong bài viết xuất sắc của Dave Cheney. Nói ngắn gọn, nó làm errors có thể tái sử dụng và bất biến hơn.
Tiếp theo, hãy tạo hàm Update để sửa đổi định nghĩa của một từ.
Viết test trước tiên
Update liên quan chặt chẽ đến Add và sẽ là implementation tiếp theo của chúng ta.
Thử chạy test
Viết lượng code tối thiểu để chạy test và kiểm tra kết quả lỗi
Chúng ta đã biết cách xử lý lỗi kiểu này. Cần định nghĩa hàm.
Với điều đó, chúng ta có thể thấy cần thay đổi định nghĩa của từ.
Viết đủ code để test chạy thành công
Chúng ta đã thấy cách làm điều này khi sửa vấn đề với Add. Hãy implement tương tự như Add.
Không cần refactor vì đây là thay đổi đơn giản. Tuy nhiên, bây giờ chúng ta có vấn đề tương tự như Add. Nếu truyền vào từ mới, Update sẽ thêm nó vào từ điển.
Viết test trước tiên
Chúng ta thêm loại lỗi mới khi từ không tồn tại. Chúng ta cũng sửa đổi Update để trả về giá trị error.
Thử chạy test
Chúng ta nhận 3 lỗi lần này, nhưng chúng ta biết cách xử lý chúng.
Viết lượng code tối thiểu để chạy test và kiểm tra kết quả lỗi
Chúng ta thêm kiểu lỗi riêng và trả về lỗi nil.
Với những thay đổi này, chúng ta nhận lỗi rõ ràng:
Viết đủ code để test chạy thành công
Hàm này trông gần giống Add ngoại trừ chúng ta đổi khi nào cập nhật dictionary và khi nào trả về lỗi.
Lưu ý về khai báo lỗi mới cho Update
Chúng ta có thể tái sử dụng ErrNotFound và không thêm lỗi mới. Tuy nhiên, thường tốt hơn là có lỗi chính xác khi update fail.
Có errors cụ thể cung cấp thêm thông tin về những gì đã xảy ra. Đây là ví dụ trong web app:
Bạn có thể redirect người dùng khi gặp ErrNotFound, nhưng hiển thị thông báo lỗi khi gặp ErrWordDoesNotExist.
Tiếp theo, hãy tạo hàm Delete từ trong từ điển.
Viết test trước tiên
Test của chúng ta tạo Dictionary với một từ rồi kiểm tra từ đó đã bị xóa.
Thử chạy test
Bằng cách chạy go test:
Viết lượng code tối thiểu để chạy test và kiểm tra kết quả lỗi
Sau khi thêm điều này, test cho biết chúng ta không xóa từ.
Viết đủ code để test chạy thành công
Go có hàm built-in delete hoạt động trên maps. Nó nhận hai đối số và không trả về gì. Đối số đầu tiên là map và đối số thứ hai là key cần xóa.
Refactor
Không có nhiều thứ cần refactor, nhưng chúng ta có thể implement logic tương tự từ Update để xử lý trường hợp từ không tồn tại.
Thử chạy test
Compiler sẽ thất bại vì chúng ta không trả về value cho Delete.
Viết đủ code để test chạy thành công
Chúng ta dùng switch statement để match trên lỗi khi cố xóa từ không tồn tại.
Tổng kết
Trong phần này, chúng ta đã học nhiều thứ. Chúng ta đã tạo API CRUD (Create, Read, Update và Delete) đầy đủ cho từ điển. Trong suốt quá trình, chúng ta học được cách:
package main
func Search(dictionary map[string]string, word string) string {
return ""
}
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)
}
}
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)
}
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
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
}
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")
if got == nil {
t.Fatal("expected to get an error.")
}
assertError(t, got, ErrNotFound)
})
var dictionary = map[string]string{}
// HOẶC
var dictionary = make(map[string]string)
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)
}
assertStrings(t, 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)
})
}
./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
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
}
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'
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)
}
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_test.go:53:2: dictionary.Update undefined (type Dictionary has no field or method Update)
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, newDefinition) used as value
./dictionary_test.go:64:16: dictionary.Update(word, definition) used as value
./dictionary_test.go:66:23: undefined: ErrWordDoesNotExist
const (
ErrNotFound = DictionaryErr("could not find the word you were looking for")
ErrWordExists = DictionaryErr("cannot add word because it already exists")
ErrWordDoesNotExist = DictionaryErr("cannot perform operation on word because it does not exist")
)
func (d Dictionary) Update(word, definition string) error {
d[word] = definition
return nil
}
dictionary_test.go:66: got error '%!q(<nil>)' want 'cannot update word because it does not exist'
./dictionary_test.go:77:10: dictionary.Delete(word) (no value) used as value
./dictionary_test.go:90:10: dictionary.Delete(word) (no value) used as value