Maps

Tất cả code của chương này được lưu tại đâyarrow-up-right

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

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")
	}
}

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 specarrow-up-right.

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ảarrow-up-right, 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 đâyarrow-up-right.

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 Cheneyarrow-up-right. 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:

  • Tạo maps

  • Tìm kiếm các mục trong maps

  • Thêm mục mới vào maps

  • Cập nhật mục trong maps

  • Xóa mục khỏi map

  • Tìm hiểu thêm về errors

    • Cách tạo errors là constants

    • Viết error wrappers

Last updated