Map trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu map là gì, cú pháp khai báo map, thêm phần tử, truy xuất phần tử, xóa phần tử, duyệt các phần tử, ... của map trong Go.

Map là gì?

Map là một kiểu tích hợp sẵn trong Go được sử dụng để lưu trữ các cặp khóa-giá trị.

Hãy lấy ví dụ về một công ty khởi nghiệp với một vài nhân viên. Để đơn giản, hãy giả sử rằng tên đầu tiên của tất cả những nhân viên này là duy nhất. Chúng ta đang tìm kiếm một cấu trúc dữ liệu để lưu trữ lương của từng nhân viên.

Map sẽ hoàn toàn phù hợp cho trường hợp sử dụng này. Tên của nhân viên có thể là khóa và tiền lương có thể là giá trị.

Map tương tự như dictionary trong các ngôn ngữ khác như C#, Java, Python.

Cú pháp khai báo map trong Go

Map có thể được tạo bằng cách truyền kiểu dữ liệu của khóa và giá trị cho hàm make. Sau đây là cú pháp để tạo một map mới.

make(map[type of key]type of value)  
employeeSalary := make(map[string]int)  

Dòng mã trên tạo một map được đặt tên là employeeSalary có khóa kiểu string và giá trị kiểu int.

package main

import (  
    "fmt"
)

func main() {  
    employeeSalary := make(map[string]int)
    fmt.Println(employeeSalary)
}

Chạy chương trình trong playground

Chương trình trên tạo ra một map được đặt tên employeeSalary với khóa kiểu string và giá trị kiểu int. Chương trình trên sẽ in ra kết quả sau:

map[]  

Vì chúng ta chưa thêm bất kỳ phần tử nào vào map nên nó trống.

Thêm phần tử vào map

Cú pháp để thêm các phần tử mới vào map cũng giống như cú pháp của mảng. Chương trình dưới đây thêm một số nhân viên mới vào map employeeSalary.

package main

import (  
    "fmt"
)

func main() {  
    employeeSalary := make(map[string]int)
    employeeSalary["steve"] = 12000
    employeeSalary["jamie"] = 15000
    employeeSalary["mike"] = 9000
    fmt.Println("employeeSalary map contents:", employeeSalary)
}

Chạy chương trình trong playground

Chúng ta đã thêm ba nhân viên steve, jamiemike và tiền lương tương ứng của họ.

Chương trình trên in ra kết quả sau:

employeeSalary map contents: map[steve:12000 jamie:15000 mike:9000]  

Cũng có thể khởi tạo một map trong quá trình khai báo chính nó.

package main

import (  
    "fmt"
)

func main() {  
    employeeSalary := map[string]int {
        "steve": 12000,
        "jamie": 15000,
    }
    employeeSalary["mike"] = 9000
    fmt.Println("employeeSalary map contents:", employeeSalary)
}

Chạy chương trình trong playground

Chương trình trên khai báo map employeeSalary và thêm hai phần tử vào nó trong quá trình khai báo chính nó. Sau đó, một phần tử nữa có khóa mike được thêm vào. Chương trình in ra kết quả sau:

employeeSalary map contents: map[jamie:15000 mike:9000 steve:12000]  

Map không nhất thiết chỉ có khóa phải là kiểu chuỗi. Tất cả các kiểu có thể so sánh được như boolean, integer, float, complex, string, ... cũng có thể làm khóa.

Ngay cả các kiểu do người dùng định nghĩa như struct cũng có thể là khóa. Nếu bạn muốn biết thêm về các kiểu có thể so sánh, vui lòng truy cập:

The Go Programming Language Specification - go.dev
Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.

Zero value của map

Zero value của map là nil. Nếu bạn cố gắng thêm các phần tử vào map nil, lỗi thời gian chạy sẽ xảy ra. Do đó, map phải được khởi tạo trước khi thêm các phần tử.

package main

func main() {  
    var employeeSalary map[string]int
    employeeSalary["steve"] = 12000
}

Chạy chương trình trong playground

Trong chương trình trên, map employeeSalarynil và chúng ta đang cố gắng thêm một khóa mới vào map. Chương trình sẽ báo lỗi:

panic: assignment to entry in nil map

Lấy giá trị cho một khóa từ map

Bây giờ chúng ta đã thêm một số phần tử vào map, hãy tìm hiểu cách truy xuất chúng. map[key] là cú pháp để truy xuất các phần tử của map.

package main

import (  
    "fmt"
)

func main() {  
    employeeSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
        "mike": 9000,
    }
    employee := "jamie"
    salary := employeeSalary[employee]
    fmt.Println("Salary of", employee, "is", salary)
}

Chạy chương trình trong playground

Chương trình trên khá đơn giản. Tiền lương của nhân viên jamie được truy xuất và in ra. Chương trình in ra kết quả sau:

Salary of jamie is 15000  

Điều gì sẽ xảy ra nếu một phần tử không có trong map? Map sẽ trả về zero value của kiểu dữ liệu của phần tử đó. Trong trường hợp map employeeSalary, nếu chúng ta cố gắng truy cập một phần tử không có trong map, zero value của phần tử kiểu int0 sẽ được trả về.

package main

import (  
    "fmt"
)

func main() {  
    employeeSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    fmt.Println("Salary of joe is", employeeSalary["joe"])
}

Chạy chương trình trong playground

Kết quả của chương trình trên là:

Salary of joe is 0  

Chương trình trên trả về mức lương của joe là 0. Sẽ không có lỗi thời gian chạy khi chúng ta cố gắng truy xuất giá trị cho một khóa không có trong map.

Kiểm tra xem khóa có tồn tại không

Trong phần trên, chúng ta đã biết rằng khi không có khóa, zero value của kiểu sẽ được trả về. Điều này không hữu ích khi chúng ta muốn tìm hiểu xem liệu khóa có thực sự tồn tại trên map hay không.

Ví dụ, chúng ta muốn biết liệu một khóa có hiện diện trong map employeeSalary hay không.

value, ok := map[key]  

Trên đây là cú pháp để tìm xem một khóa cụ thể có trong map hay không. Nếu ok là true, thì khóa có mặt và giá trị của nó có trong biến value, ngược lại khóa sẽ không có.

package main

import (  
    "fmt"
)

func main() {  
    employeeSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    newEmp := "joe"
    value, ok := employeeSalary[newEmp]
    if ok == true {
        fmt.Println("Salary of", newEmp, "is", value)
        return
    }
    fmt.Println(newEmp, "not found")

}

Chạy chương trình trong playground

Trong chương trình trên, ở dòng số 13, ok sẽ là false vì khóa joe không có mặt trong map. Do đó chương trình sẽ in ra kết quả sau:

joe not found  

Duyệt qua tất cả các phần tử trong map

Mẫu range của vòng lặp for được sử dụng để duyệt qua trên tất cả các phần tử của map.

package main

import (  
    "fmt"
)

func main() {  
    employeeSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
        "mike":  9000,
    }
    fmt.Println("Contents of the map")
    for key, value := range employeeSalary {
        fmt.Printf("employeeSalary[%s] = %d\n", key, value)
    }

}

Chạy chương trình trong playground

Kết quả chương trình trên là:

Contents of the map  
employeeSalary[mike] = 9000  
employeeSalary[steve] = 12000  
employeeSalary[jamie] = 15000  
Một thực tế quan trọng là thứ tự truy xuất các giá trị từ map khi sử dụng for range không được đảm bảo là giống nhau cho mỗi lần thực thi chương trình. Nó cũng không giống với thứ tự mà các phần tử được thêm vào map

Xóa phần tử khỏi map

delete (map, key) là cú pháp để xóa khóa key khỏi map. Hàm xóa không trả lại bất kỳ giá trị gì.

package main

import (  
    "fmt"
)

func main() {  
    employeeSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,     
        "mike": 9000,
    }
    fmt.Println("map before deletion", employeeSalary)
    delete(employeeSalary, "steve")
    fmt.Println("map after deletion", employeeSalary)

}

Chạy chương trình trong playground

Chương trình trên xóa khóa steve khỏi map và in ra kết quả sau:

map before deletion map[steve:12000 jamie:15000 mike:9000]  
map after deletion map[mike:9000 jamie:15000]  

Nếu chúng tôi cố gắng xóa một khóa không có trong map, sẽ không có lỗi thời gian chạy.

Map kiểu struct

Cho đến nay chúng ta chỉ lưu trữ lương của nhân viên trong map. Sẽ thật tuyệt nếu chúng ta cũng có thể lưu trữ quốc gia của từng nhân viên trong map phải không?

Điều này có thể đạt được bằng cách sử dụng map kiểu struct. Nhân viên có thể được biểu diễn dưới dạng struct chứa các trường lương và quốc gia và chúng sẽ được lưu trữ trong map với khóa kiểu chuỗi và giá trị kiểu struct.

Hãy viết một chương trình để hiểu làm thế nào điều này có thể được thực hiện.

package main

import (  
    "fmt"
)

type employee struct {  
    salary  int
    country string
}

func main() {  
    emp1 := employee{
        salary:  12000,
        country: "USA",
    }
    emp2 := employee{
        salary:  14000,
        country: "Canada",
    }
    emp3 := employee{
        salary:  13000,
        country: "India",
    }
    employeeInfo := map[string]employee{
        "Steve": emp1,
        "Jamie": emp2,
        "Mike":  emp3,
    }

    for name, info := range employeeInfo {
        fmt.Printf("Employee: %s Salary:$%d  Country: %s\n", name, info.salary, info.country)
    }

}

Chạy chương trình trong playground

Trong chương trình trên, struct employee chứa các trường salarycountry. Chúng ta tạo ra ba nhân viên emp1, emp2emp3.

Trong dòng số 25, chúng ta khởi tạo một map với loại khóa kiểu string và giá trị kiểu employee với ba nhân viên mà chúng ta đã tạo.

Map được duyệt qua ở dòng số 31 và chi tiết nhân viên được in ở dòng tiếp theo. Chương trình này sẽ in ra kết quả sau:

Employee: Mike Salary:$13000  Country: India  
Employee: Steve Salary:$12000  Country: USA  
Employee: Jamie Salary:$14000  Country: Canada  

Độ dài của map

Độ dài của map có thể được xác định bằng cách sử dụng hàm len.

package main

import (  
    "fmt"
)

func main() {  
    employeeSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,
    }
    fmt.Println("length is", len(employeeSalary))

}

Chạy chương trình trong playground

Hàm len(workerSalary) trong chương trình trên trả về độ dài của map. Chương trình trên in ra kết quả sau:

length is 2  

Map là kiểu tham chiếu

Tương tự như slice, map là kiểu tham chiếu. Khi một map được gán cho một biến mới, cả hai đều trỏ đến cùng một cấu trúc dữ liệu nội bộ. Do đó, những thay đổi được thực hiện ở cái này sẽ phản ánh ở cái kia.

package main

import (  
    "fmt"
)

func main() {  
    employeeSalary := map[string]int{
        "steve": 12000,
        "jamie": 15000,     
        "mike": 9000,
    }
    fmt.Println("Original employee salary", employeeSalary)
    modified := employeeSalary
    modified["mike"] = 18000
    fmt.Println("Employee salary changed", employeeSalary)

}

Chạy chương trình trong playground

Trong dòng số 14 của chương trình trên, map employeeSalary được gán cho map modified. Trong dòng tiếp theo, mức lương của mike được thay đổi thành 18000 trong map modified. Lương của Mike ở trong map employeeSalary bây giờ cũng sẽ bằng 18000. Chương trình đầu ra:

Original employee salary map[jamie:15000 mike:9000 steve:12000]  
Employee salary changed map[jamie:15000 mike:18000 steve:12000]  

Tương tự là trường hợp khi các map được truyền dưới dạng tham số cho các hàm. Khi bất kỳ thay đổi nào được thực hiện đối với map bên trong hàm, nó cũng sẽ thay đổi map bên ngoài hàm.

So sánh hai map

Map không thể được so sánh bằng cách sử dụng toán tử ==. Chỉ có thể được sử dụng == để kiểm tra xem một map có phải là nil hay không.

package main

func main() {  
    map1 := map[string]int{
        "one": 1,
        "two": 2,
    }

    map2 := map1

    if map1 == map2 {
    }
}

Chạy chương trình trong playground

Chương trình trên sẽ không biên dịch được với lỗi

invalid operation: map1 == map2 (map can only be compared to nil)  

Một cách để kiểm tra xem hai map có bằng nhau hay không là so sánh từng phần tử riêng lẻ của từng map một. Cách khác là sử dụng reflection. Tôi khuyến khích bạn viết một chương trình cho điều này và làm cho nó hoạt động :).

Trong hướng dẫn tiếp theo, chúng ta sẽ tìm hiểu về chuỗi trong Go.

Chuỗi trong Go
Chuỗi (string) xứng đáng được đề cập đặc biệt trong Go vì chúng khác biệt trong cách triển khai khi so sánh với các ngôn ngữ khác.

Cảm ơn bạn đã đọc.

Go
Bài Viết Liên Quan:
Phương thức (method) trong Go
Trung Nguyen 02/12/2021
Phương thức (method) trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu phương thức (method) trong Go là gì? Cú pháp khai báo phương thức, so sánh phương thức với hàm, ... trong Go.

Struct trong Go
Trung Nguyen 28/11/2021
Struct trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu struct là gì, cách khai báo và sử dụng một struct trong Go, struct ẩn danh, so sanh hai struct, ...

Con trỏ trong Go
Trung Nguyen 28/11/2021
Con trỏ trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách con trỏ (pointer) hoạt động trong Go và nó khác với con trỏ trong các ngôn ngữ khác như C và C++ như thế nào.

Chuỗi trong Go
Trung Nguyen 28/11/2021
Chuỗi trong Go

Chuỗi (string) xứng đáng được đề cập đặc biệt trong Go vì chúng khác biệt trong cách triển khai khi so sánh với các ngôn ngữ khác.