1. Tổng quan.
Trong ngôn ngữ Go (Golang), các map được triển khai dưới dạng hash table. Khi bạn thêm một cặp khóa-giá trị vào map, một hàm băm sẽ được sử dụng để xác định vị trí nơi giá trị sẽ được lưu trữ. Khi bạn truy vấn giá trị cho một khóa cụ thể, hàm băm sẽ được sử dụng lại để nhanh chóng xác định bucket chứa giá trị đó.
Một hash table (bảng băm) là một cấu trúc dữ liệu được thiết kế để lưu trữ dữ liệu theo cặp khóa-giá trị. Hash table sử dụng một hàm băm để ánh xạ mỗi khóa (key) tới một vị trí cụ thể trong bảng. Vị trí này thường được gọi là “bucket” hoặc “slot”. Sau đó, giá trị tương ứng với khóa đó được lưu trữ trong bucket đó.
- Cách hoạt động cơ bản:
- Băm (Hashing): Một hàm băm chuyển đổi mỗi khóa thành một giá trị số nguyên, thường là một số nguyên dương. Hàm băm cố gắng phân tán các khóa đều trên toàn bộ không gian bucket.
- Ánh xạ (Mapping): Giá trị được lưu trữ trong bucket được xác định bởi kết quả của hàm băm. Điều này giúp tìm kiếm nhanh chóng vì nếu bạn biết khóa, bạn có thể sử dụng hàm băm để nhanh chóng xác định bucket chứa giá trị tương ứng.
- Các thuộc tính của Maps:
- Maps được sử dụng để lưu trữ giá trị dưới dạng cặp key:value.
- Mỗi phần tử trong một map là một cặp key:value.
- Maps là một tập hợp không thứ tự và có thể thay đổi, không cho phép các key trùng lặp.
- Số lượng phần tử trong map có thể được xác định bằng hàm
len()
. - Giá trị mặc định của một map là
nil
. - Maps thực sự giữ tham chiếu đến một hash table, nơi mà các key được ánh xạ thành các giá trị.
- Go hỗ trợ nhiều cách để khởi tạo maps, bao gồm cả cách tạo mới một map trống và cách khởi tạo map có giá trị ban đầu.
2. Tạo map bằng cách sử dụng cả var
và :=
.
Tạo Map bằng var
sử dụng khi bạn muốn chỉ khai báo một map mà không khởi tạo giá trị ngay lập tức hoặc sử dụng var
để khai báo map và sau đó sử dụng make
để tạo một map mới khi cần.
var myMap map[string]int
myMap = make(map[string]int)
hoặc ngắn gọn hơn:
var myMap = make(map[string]int)
Tạo Map bằng :=
sử dụng khi bạn muốn khai báo và khởi tạo một map ngay từ khi khai báo hoặc sử dụng :=
để tạo map và gán giá trị ngay từ dòng khai báo.
myMap := map[string]int{"one": 1, "two": 2, "three": 3}
Cách này ngắn gọn và thường được ưa chuộng khi bạn có thể cung cấp các giá trị khởi tạo ngay từ đầu.
Xem ví dụ dưới, chúng ta sẽ tạo map bằng cách sử dụng cả var
và :=
.
package main
import ("fmt")
func main() {
var a = map[string]string{"brand": "Ford", "model": "Mustang", "year": "1964"}
b := map[string]int{"Oslo": 1, "Bergen": 2, "Trondheim": 3, "Stavanger": 4}
fmt.Printf("a\t%v\n", a)
fmt.Printf("b\t%v\n", b)
}
Ở ví dụ trên chúng ta sử dụng var
để khai báo và tạo map:
var a = map[string]string{"brand": "Ford", "model": "Mustang", "year": "1964"}
Ở đây, var a
được sử dụng để khai báo một map với key và value đều là string và sau đó cung cấp giá trị ban đầu cho map bằng cách liệt kê các cặp key:value.
Tiếp theo mình sử dụng :=
để tạo map ngắn gọn:
b := map[string]int{"Oslo": 1, "Bergen": 2, "Trondheim": 3, "Stavanger": 4}
Trong trường hợp này, :=
được sử dụng để tạo một map mới với kiểu key là string và kiểu value là int. Các cặp key:value cũng được liệt kê ngay sau :=
.
Code trên in ra giá trị của map a
và b
sử dụng hàm Printf
.
fmt.Printf("a\t%v\n", a)
fmt.Printf("b\t%v\n", b)
Kết quả in ra màn hình sẽ là:
a map[brand:Ford model:Mustang year:1964]
b map[Bergen:2 Oslo:1 Stavanger:4 Trondheim:3]
Lưu ý là maps trong Go là một cấu trúc dữ liệu không thứ tự và có khả năng thay đổi nên thứ tự các phần tử trong map không nhất thiết phản ánh thứ tự lưu trữ thực tế của chúng trong map.
3. Tạo map bằng cách sử dụng hàm make()
.
Tạo Map bằng make
sử dụng khi bạn muốn tạo map với số lượng phân tử mong muốn cụ thể hoặc sử dụng make
để tạo map và chỉ định số lượng phân tử (capacity) khi cần.
myMap := make(map[string]int, 10) // Số lượng phân tử mong muốn ban đầu là 10
Điều này có thể hữu ích nếu bạn biết trước rằng map có thể chứa một số lượng cụ thể các cặp khóa-giá trị và bạn muốn tránh việc cấp phát bộ nhớ nhiều lần.
Theo dõi ví dụ dưới đây, chúng ta thấy cách tạo map bằng cách sử dụng hàm make()
.
package main
import ("fmt")
func main() {
var a = make(map[string]string) // The map is empty now
a["brand"] = "Ford"
a["model"] = "Mustang"
a["year"] = "1964"
// a is no longer empty
b := make(map[string]int)
b["Oslo"] = 1
b["Bergen"] = 2
b["Trondheim"] = 3
b["Stavanger"] = 4
fmt.Printf("a\t%v\n", a)
fmt.Printf("b\t%v\n", b)
}
Ở đây, make(map[string]string)
được sử dụng để tạo một map mới với key và value đều là string. Map a
bây giờ là một map trống.
var a = make(map[string]string) // Tạo một map rỗng
Chúng ta gán giá trị cho các cặp key:value trong map a
.
a["brand"] = "Ford"
a["model"] = "Mustang"
a["year"] = "1964"
make(map[string]int)
được sử dụng để tạo một map mới với key là string và value là int. Map b
hiện đang là một map trống.
b := make(map[string]int)
Chúng ta gán giá trị cho các cặp key:value trong map b
.
b["Oslo"] = 1
b["Bergen"] = 2
b["Trondheim"] = 3
b["Stavanger"] = 4
Đoạn code trên in ra giá trị của map a
và b
sử dụng hàm Printf
.
fmt.Printf("a\t%v\n", a)
fmt.Printf("b\t%v\n", b)
Kết quả in ra màn hình sẽ là:
a map[brand:Ford model:Mustang year:1964]
b map[Bergen:2 Oslo:1 Stavanger:4 Trondheim:3]
Lưu ý rằng một khi chúng ta đã thêm giá trị vào map a
và b
thì chúng không còn là map trống nữa.
4. Tạo map rỗng.
Trong ví dụ này, chúng ta thấy sự khác biệt giữa việc khai báo một map rỗng bằng cách sử dụng make()
và không sử dụng make()
.
package main
import ("fmt")
func main() {
var a = make(map[string]string)
var b map[string]string
fmt.Println(a == nil)
fmt.Println(b == nil)
}
Ở đây, make(map[string]string)
được sử dụng để tạo một map mới với key và value đều là string. Map a
bây giờ là một map trống.
var a = make(map[string]string)
Trong trường hợp này var b map[string]string
chỉ khai báo một biến map b
nhưng không tạo một map thực sự, b
hiện đang là nil
(một giá trị zero cho map).
var b map[string]string
a == nil
trả về false
vì a
là một map thực sự được tạo bằng make()
. Ngược lại, b == nil
trả về true
vì b
chỉ là một biến map đã được khai báo nhưng chưa được tạo.
fmt.Println(a == nil)
fmt.Println(b == nil)
Kết quả của ví dụ trên là
false
true
Khi bạn muốn tạo một map trống, việc sử dụng make()
là cách đúng vì nếu bạn tạo một map mà không sử dụng make()
và ghi dữ liệu vào nó, điều này có thể gây ra lỗi runtime panic.
5. Các kiểu dữ liệu được phép và không được phép sử dụng làm key trong maps.
Phần này sẽ giải thích về các kiểu dữ liệu được phép và không được phép sử dụng làm key trong maps, cũng như cách truy cập các phần tử trong maps.
- Các kiểu dữ liệu được phép:
- Booleans
- Numbers
- Strings
- Arrays
- Pointers
- Structs
- Interfaces (miễn là kiểu động tương ứng hỗ trợ toán tử so sánh
==
)
- Các kiểu dữ liệu không được phép:
- Slices
- Maps
- Functions
Các kiểu dữ liệu không được phép là do toán tử so sánh ==
không được định nghĩa cho chúng.
6. Truy cập giá trị trong map.
Để truy cập giá trị của một phần tử trong map, bạn sử dụng cú pháp sau:
value = map_name[key]
Ví dụ:
package main
import "fmt"
func main() {
var a = make(map[string]string)
a["brand"] = "Ford"
a["model"] = "Mustang"
a["year"] = "1964"
fmt.Printf(a["brand"])
}
Kết quả khi chạy chương trình sẽ là:
Ford
Ở đây, a["brand"]
được sử dụng để truy cập giá trị của phần tử có key là “brand” trong map a
.
7. Cập nhật và thêm phần tử trong maps.
Cập nhật và thêm phần tử trong maps được thực hiện bằng cách sử dụng cú pháp map_name[key] = value
.
Cập nhật một phần tử:
map_name[key] = new_value
Thêm một phần tử mới:
map_name[new_key] = new_value
Ví dụ:
package main
import "fmt"
func main() {
var a = make(map[string]string)
a["brand"] = "Ford"
a["model"] = "Mustang"
a["year"] = "1964"
fmt.Println(a)
a["year"] = "1970" // Cập nhật một phần tử
a["color"] = "red" // Thêm một phần tử mới
fmt.Println(a)
}
Kết quả khi chạy chương trình sẽ là:
map[brand:Ford model:Mustang year:1964]
map[brand:Ford color:red model:Mustang year:1970]
Ở đây, chúng ta cập nhật giá trị của phần tử có key là “year” từ “1964” thành “1970” và thêm một phần tử mới với key là “color” và giá trị là “red”. Kết quả là map a
được cập nhật và mở rộng với các phần tử mới.
8. Xóa phần tử trong map.
Để xóa một phần tử từ map, bạn sử dụng hàm delete()
.
delete(map_name, key)
Ví dụ:
package main
import "fmt"
func main() {
var a = make(map[string]string)
a["brand"] = "Ford"
a["model"] = "Mustang"
a["year"] = "1964"
fmt.Println(a)
delete(a, "year")
fmt.Println(a)
}
Kết quả khi chạy chương trình sẽ là:
map[brand:Ford model:Mustang year:1964]
map[brand:Ford model:Mustang]
Ở đây, chúng ta sử dụng delete(a, "year")
để xóa phần tử có key là “year” từ map a
. Sau khi thực hiện lệnh này, phần tử “year” đã không còn tồn tại trong map và map a
được cập nhật mà không chứa phần tử có key là “year”.
9. Kiểm tra xem một key cụ thể có tồn tại trong một map hay không?
Để kiểm tra xem một key cụ thể có tồn tại trong một map hay không, bạn có thể sử dụng cú pháp:
val, ok := map_name[key]
Nếu bạn chỉ muốn kiểm tra sự tồn tại của một key cụ thể mà không quan tâm đến giá trị của key đó, bạn có thể sử dụng blank identifier (_
) thay cho val
.
Ví dụ:
package main
import "fmt"
func main() {
var a = map[string]string{"brand": "Ford", "model": "Mustang", "year": "1964", "day": ""}
val1, ok1 := a["brand"] // Kiểm tra sự tồn tại của key và giá trị của key
val2, ok2 := a["color"] // Kiểm tra sự tồn tại của key không tồn tại và giá trị của key
val3, ok3 := a["day"] // Kiểm tra sự tồn tại của key và giá trị của key
_, ok4 := a["model"] // Chỉ kiểm tra sự tồn tại của key, không quan trọng giá trị của key
fmt.Println(val1, ok1)
fmt.Println(val2, ok2)
fmt.Println(val3, ok3)
fmt.Println(ok4)
}
Kết quả khi chạy chương trình sẽ là:
Ford true
false
true
true
Ở đây:
val1, ok1 := a["brand"]
kiểm tra sự tồn tại của key “brand” và gán giá trị choval1
là “Ford” vàok1
làtrue
vì key “brand” tồn tại.val2, ok2 := a["color"]
kiểm tra sự tồn tại của key “color” và gán giá trị choval2
là một empty string vàok2
làfalse
vì key “color” không tồn tại.val3, ok3 := a["day"]
kiểm tra sự tồn tại của key “day” và gán giá trị choval3
là một empty string vàok3
làtrue
vì key “day” tồn tại._, ok4 := a["model"]
chỉ kiểm tra sự tồn tại của key “model” và gán giá trị chook4
làtrue
vì key “model” tồn tại. Giá trị của key không được sử dụng (_
).
10. Maps trong Go được thực hiện dưới dạng tham chiếu đến các bảng hash tables.
Maps trong Go được thực hiện dưới dạng tham chiếu đến các bảng hash tables. Điều này có nghĩa là nếu hai biến map trỏ đến cùng một bảng hash tables, việc thay đổi nội dung của một biến sẽ ảnh hưởng đến nội dung của biến còn lại.
Ví dụ:
package main
import "fmt"
func main() {
var a = map[string]string{"brand": "Ford", "model": "Mustang", "year": "1964"}
b := a
fmt.Println(a)
fmt.Println(b)
b["year"] = "1970"
fmt.Println("After change to b:")
fmt.Println(a)
fmt.Println(b)
}
Kết quả khi chạy chương trình sẽ là:
map[brand:Ford model:Mustang year:1964]
map[brand:Ford model:Mustang year:1964]
After change to b:
map[brand:Ford model:Mustang year:1970]
map[brand:Ford model:Mustang year:1970]
Ở đây, a
và b
trỏ đến cùng một bảng hash tables ban đầu. Khi giá trị của phần tử có key là “year” được thay đổi thông qua biến b
, giá trị tương ứng trong bảng hash tables cũng thay đổi. Do đó, khi in ra giá trị của a
và b
sau khi thay đổi, bạn thấy rằng cả hai đều được cập nhật để phản ánh sự thay đổi.
11. Lặp qua các phần tử trong một map.
Để lặp qua các phần tử trong một map, bạn có thể sử dụng range
. Dưới đây là ví dụ về cách lặp qua các phần tử trong một map:
package main
import "fmt"
func main() {
a := map[string]int{"one": 1, "two": 2, "three": 3, "four": 4}
for k, v := range a {
fmt.Printf("%v : %v, ", k, v)
}
}
Kết quả khi chạy chương trình sẽ là:
two : 2, three : 3, four : 4, one : 1,
Lưu ý rằng thứ tự của các phần tử trong map khi lặp qua có thể không theo thứ tự được đặt trong mã nguồn. Maps trong Go là một cấu trúc dữ liệu không thứ tự và có khả năng thay đổi, nghĩa là thứ tự khi lặp qua có thể thay đổi mỗi lần bạn chạy chương trình.
12. Lặp dữ liệu trong map.
Vì maps trong Go là cấu trúc dữ liệu không thứ tự, nếu bạn cần lặp qua map theo một thứ tự cụ thể, bạn phải sử dụng một cấu trúc dữ liệu khác để chỉ định thứ tự đó.
Trong ví dụ dưới đây, một slice được sử dụng để xác định thứ tự mong muốn:
package main
import "fmt"
func main() {
a := map[string]int{"one": 1, "two": 2, "three": 3, "four": 4}
var b []string // Xác định thứ tự mong muốn
b = append(b, "one", "two", "three", "four")
// Lặp qua map mà không có thứ tự cụ thể
for k, v := range a {
fmt.Printf("%v : %v, ", k, v)
}
fmt.Println()
// Lặp qua map với thứ tự được xác định bởi slice
for _, element := range b {
fmt.Printf("%v : %v, ", element, a[element])
}
}
Kết quả khi chạy chương trình sẽ là:
two : 2, three : 3, four : 4, one : 1,
one : 1, two : 2, three : 3, four : 4,
Ở đây, slice b
được sử dụng để xác định thứ tự mong muốn của keys trong map. Đầu tiên, chúng ta lặp qua map a
mà không có thứ tự cụ thể. Sau đó, chúng ta lặp lại qua map với thứ tự được xác định bởi slice b
. Điều này đảm bảo rằng chúng ta lặp qua map theo thứ tự mà chúng ta mong muốn.
12. Ví dụ thực tế 1.
Mình sẽ tạo một list ipaddrs với danh sách IP và các thông tin bổ sung liên quan đến IP đó. Sau đó mình sẽ lặp qua các phần tử của map và kiểm tra xem mảng details có ít nhất 2 phần tử không:
- Trường hợp 1: Nếu đủ điều kiện ít nhất 2 phần tử thì in thông tin theo định dạng đã định danh.
- Trường hợp 2: Nếu Không đủ thông tin cho IP thì in ra thông tin IP đó.
package main
import "fmt"
func main() {
ipaddrs := map[string][]string{
"113.161.201.163": {"HoangHD", "wiki.hoanghd.com"},
"1.1.1.1": {"Cloudflare", "DNS Cloudflare"},
"8.8.8.8": {"Google", "Google DNS 1"},
"8.8.4.4": {"Google", "Google DNS 2"},
}
// Lặp qua các phần tử của map
for ip, details := range ipaddrs {
// Kiểm tra xem mảng details có ít nhất 2 phần tử không
if len(details) >= 2 {
// In thông tin theo định dạng yêu cầu
fmt.Printf("IP: %s, project: %s, description: %s\n", ip, details[0], details[1])
} else {
fmt.Printf("Không đủ thông tin cho IP: %s\n", ip)
}
}
}
Và dưới đây là kết quả.
$ go run test.go
IP: 1.1.1.1, project: Cloudflare, description: DNS Cloudflare
IP: 8.8.8.8, project: Google, description: Google DNS 1
IP: 8.8.4.4, project: Google, description: Google DNS 2
IP: 113.161.201.163, project: HoangHD, description: wiki.hoanghd.com
12. Ví dụ thực tế 2.
Ví dụ tiếp theo dùng để kiểm tra ICMP của một IP và in ra thông tin liên quan đến IP đó.
package main
import (
"os/exec"
"regexp"
"fmt"
"sync"
)
func main() {
// Danh sách các địa chỉ IP cần kiểm tra
ipAddresses := map[string][]string{
"113.161.201.163": {"HoangHD", "wiki.hoanghd.com"},
"1.1.1.1": {"Cloudflare", "DNS Cloudflare"},
"8.8.8.8": {"Google", "Google DNS 1"},
"8.8.4.4": {"Google", "Google DNS 2"},
"8.8.8.1": {"Google", "Google DNS 3"},
}
// Sử dụng WaitGroup để đợi tất cả các goroutine hoàn thành
var wg sync.WaitGroup
// Vòng lặp qua từng địa chỉ IP trong danh sách
for ipaddr, details := range ipAddresses {
// Tăng đếm cho mỗi goroutine mới
wg.Add(1)
// Goroutine để thực hiện kiểm tra cho mỗi IP
go func(ip string, details []string) {
defer wg.Done()
// Thực hiện lệnh ping và lấy kết quả đầu ra
var count string = "1"
cmd := exec.Command("ping", "-c", count, ip)
out, err := cmd.CombinedOutput()
// Kiểm tra lỗi trước khi xử lý đầu ra
if err != nil {
// fmt.Printf("Error while pinging %s: %s\n", ip, err)
// Sử dụng giá trị 1 để thể hiện lỗi khi ping
packetLoss := "0"
result := fmt.Sprintf("icmp_status{project=\"%s\", ipaddr=\"%s\", description=\"%s\"} %s\n", details[0], ip, details[1], packetLoss)
fmt.Printf(result)
return
}
// Chuyển đổi đầu ra thành chuỗi
output := string(out)
// Sử dụng regex để tìm kiếm giá trị "0%" trong đoạn văn bản
re := regexp.MustCompile(`(\d+)% packet loss`)
matches := re.FindStringSubmatch(output)
// Kiểm tra nếu có matches và lấy giá trị tương ứng
if len(matches) > 1 {
// packetLoss := matches[1]
packetLoss := "1"
result := fmt.Sprintf("icmp_status{project=\"%s\", ipaddr=\"%s\", description=\"%s\"} %s\n", details[0], ip, details[1], packetLoss)
fmt.Printf(result)
} else {
fmt.Printf("Không tìm thấy thông tin về packet loss cho IP: %s\n", ip)
}
}(ipaddr, details)
}
// Đợi cho tất cả các goroutine hoàn thành trước khi kết thúc chương trình
wg.Wait()
}
Kết quả.
$ go run main.go
icmp_status{project="HoangHD", ipaddr="113.161.201.163", description="wiki.hoanghd.com"} 1
icmp_status{project="Google", ipaddr="8.8.4.4", description="Google DNS 2"} 1
icmp_status{project="Google", ipaddr="8.8.8.8", description="Google DNS 1"} 1
icmp_status{project="Cloudflare", ipaddr="1.1.1.1", description="DNS Cloudflare"} 1
icmp_status{project="Google", ipaddr="8.8.8.1", description="Google DNS 3"} 0
13. So sánh Map trong Golang với Dictionary trong Python.
Trong Python, dict
là một cấu trúc dữ liệu chính để lưu trữ các cặp key-value, còn trong Go, map
là một cấu trúc dữ liệu tương tự. Tuy nhiên, có một số sự khác biệt quan trọng giữa map
trong Go và dict
trong Python.
Go Map.
map
trong Go là một cấu trúc dữ liệu được thiết kế để lưu trữ các cặp key-value.- Key và value trong
map
có thể là bất kỳ kiểu dữ liệu nào (trừ các kiểu dữ liệu không thể so sánh như slice, map, và function). - Để khai báo và sử dụng một
map
trong Go, bạn sử dụng cú pháp như sau:
// Khai báo một map với key là string và value là int
myMap := make(map[string]int)
// Gán giá trị cho key "example"
myMap["example"] = 42
// Truy cập giá trị của key "example"
value := myMap["example"]
Python Dict.
dict
trong Python cũng là một cấu trúc dữ liệu key-value.- Key trong
dict
có thể là bất kỳ kiểu dữ liệu không thay đổi nào, trong khi value có thể là bất kỳ kiểu dữ liệu nào. - Để khai báo và sử dụng một
dict
trong Python, bạn có thể sử dụng cú pháp như sau:
# Khai báo một dict với key là string và value là int
my_dict = {}
# Gán giá trị cho key "example"
my_dict["example"] = 42
# Truy cập giá trị của key "example"
value = my_dict["example"]
Kết luận.
- Cả
map
trong Go vàdict
trong Python đều là cấu trúc dữ liệu key-value. - Go Map cho phép bạn chọn key và value từ bất kỳ kiểu dữ liệu nào, trong khi Python Dict có thể chứa key là bất kỳ kiểu dữ liệu không thay đổi nào và value có thể là bất kỳ kiểu dữ liệu nào.
- Cú pháp và cách sử dụng có sự khác biệt, nhưng ý nghĩa chung của chúng tương tự nhau.