+8

Phần 4: Khám phá golang - Pointer, Structs, Slices, và Maps

Giới thiệu

Ở bài này mình sẽ giới thiệu những cấu trúc biến khác được phát triển từ các loại biến cơ bản và sẽ được sử dụng rất nhiều ở các bài toán thực tế. Bài này sẽ tương đối dài nên mình không dài dòng nữa, bắt đầu thôi 😄

Nội dung

Pointer

Đây là khái niệm mà hẳn các bạn từng học C/C++ đã biết và có thể sẽ khá "lú" về nó 😄 Vậy nên mình sẽ cố gắng trình bày một cách gọn gàng dễ hiểu nhất nhé.
Hiểu một các đơn giản thì con trỏ cũng là một loại biến nhưng đặc biệt ở chỗ nó được dùng để lưu trữ địa chỉ của một biến khác trong bộ nhớ RAM chứ không lưu trữ một giá trị cụ thể nào cả.
Hãy giả sử bạn đang xây dựng một nhà, và bạn có một bản vẽ hướng dẫn vị trí của mỗi phòng. Con trỏ giống như một mũi bút, chỉ vào vị trí cụ thể trên bản vẽ.
Khai báo con trỏ:

  • Kiểu khai báo con trỏ nhưng không gán địa chỉ nào cho nó, lúc này giá trị default của nó là nil. Ví dụ:

    func main() {
        var p *int
    
        fmt.Println("p = ", p) // Output: p = <nil>
    }
    

    Biến p được khai báo là một con trỏ nhưng chưa được gán giá trị nào nên giá trị default của nó là nil.

  • Kiểu khai báo và gán địa chỉ, toán tử & được sử dụng để lấy địa chỉ của một biến. Ví dụ:

    var i = 10 // khai báo biến i có giá trị bằng 10
    var p = &i // gán địa chỉ biến i cho p kiểu *int, lúc này p là con trỏ lưu địa chỉ biến i
    
  • Toán tử * sẽ chỉ ra giá trị của cái địa chỉ lưu trên con trỏ.

    fmt.Println(*p) // đọc giá trị của i thông qua con trỏ p
    *p = 21         // thiết lập giá trị của i thông qua con trỏ p
    

    Ví dụ tổng quan:

    package main
    
    import "fmt"
    
    func main() {
        var a = 10 // B1: Gán giá trị biến a = 10
        var p = &a // B2: gián địa chỉ biến a cho p kiểu *int => p là con trỏ lưu địa chỉ biến a
    
        fmt.Println("a = ", a) // Giá trị của biến a => 10
        fmt.Println("p = ", p) // Địa chỉ của biến a => 0xc00001d0d0
        fmt.Println("*p = ", *p) // Hiển thị giá trị lưu tại địa chỉ p, vì biến a lưu trữ trên địa chỉ này nên *p hiển thị giá trị của biến a => 10
    
        *p = 20 // Vì giá trị lưu ở địa chỉ 0xc00001d0d0 đã thay đổi từ 10 thành 20 nên lúc này biến a có giá trị 20 => 20
        fmt.Println("a (after) = ", a) 
    }
    

    Output:

        a =  10
        p =  0xc00001d0d0
        *p =  20
        a (after) =  20
    

Structs

  • Struct là một tập hợp các trường.

    package main
    
    import "fmt"
    
    type Vertex struct {
        X int
        Y int
    }
    
    func main() {
        fmt.Println(Vertex{1, 2})
        // Output: {1 2}
    }
    
  • Các trường được truy cập bằng cách sử dụng dấu chấm.

    package main
    
    import "fmt"
    
    type Vertex struct {
        X int
        Y int
    }
    
    func main() {
        v := Vertex{1, 2}
        v.X = 4 // Thay đổi giá trị của X trong struct Vertex
        fmt.Println(v.X)
        // Output: 4
    }
    
  • Pointers to structs - Các trường có thể được truy cập thông qua một struct pointer.

    package main
    
    import "fmt"
    
    type Vertex struct {
        X int
        Y int
    }
    
    func main() {
        v := Vertex{1, 2}
        p := &v
        p.X = 3
        fmt.Println(v)
        // Output: {3 2}
    }
    

    Để truy cập trường X của struct Vertex khi chúng ta có một struct pointer là p, chúng ta có thể viết (*p).X. Tuy nhiên nhìn nó khác cồng kềnh, vì thế nên Go cho phép chúng ta viết p.X là được.

    Ví dụ mở rộng:

        package main
    
        import "fmt"
    
        type Vertex struct {
            X, Y int
        }
    
        var (
            v1 = Vertex{1, 2}  // v1 có type là struct Vertex
            v2 = Vertex{X: 1}  // Chỉ gán cho X, ngầm gán giá trị defaul Y:0
            v3 = Vertex{}      // X:0 và Y:0
            p  = &Vertex{1, 2} // có type là *Vertex
        )
    
        func main() {
            fmt.Println(v1, p, v2, v3)
            // Output: {1 2} &{1 2} {1 0} {0 0}
        }
    

Arrays

  • Mảng trong Go được khai báo như sau: [n]T => Mảng gồm n giá trị của kiểu T
    Ví dụ: var a [10]int => khai báo một biến a là một mảng có 10 số nguyên.
  • Độ dài của mảng là một phần trong khai báo, vì thế nên mảng không thể thay đổi được kích thước.
    package main
    
    import "fmt"
    
    func main() {
        var a [2]string
        a[0] = "Hello"
        a[1] = "Golang"
        fmt.Println(a[0], a[1])
        fmt.Println(a)
    
        primes := [6]int{2, 3, 5, 7, 11, 13}
        fmt.Println(primes)
    }
    
    Output:
     Hello Golang
     [Hello Golang]
     [2 3 5 7 11 13]
    

Slices

  • Một mảng có kích thước cố định. VÌ vậy slice ra đời để mang lại sự linh hoạt hơn về kích thước. Trong thực tế thì slice được sử dụng nhiều hơn so với mảng.

  • Khai báo slice: []T => một slice với các phần tử có kiểu T . Một slice được hình thành bằng cách chỉ định hai chỉ số, một giới hạn thấp và cao, cách nhau bởi một dấu hai chấm: a[thấp : cao]

    package main
    
    import "fmt"
    
    func main() {
        primes := [6]int{2, 3, 5, 7, 11, 13}
    
        var s []int = primes[1:4]
        fmt.Println(s) // Output: [3 5 7]
    }
    
  • Thay đổi các phần tử trong slice, khi hay đổi các phần tử của một slice thì sẽ sửa đổi các phần tử tương ứng của mảng cơ sở.

    package main
    
    import "fmt"
    
    func main() {
        names := [4]string{
            "John",
            "Paul",
            "George",
            "Ringo",
        }
        fmt.Println(names)   // [John Paul George Ringo]
    
        a := names[0:2]   // [John Paul] 
        b := names[1:3]   // [Paul George]
        fmt.Println(a, b)
    
        b[0] = "XXX"
        fmt.Println(a, b) [John XXX] [XXX George]
        fmt.Println(names) [John XXX George Ringo] => slice names bị thay đổi theo slice b 
    }
    
  • Slice literals: Tương tự như cú pháp của mảng nhưng không có chiều dài được chỉ định.

        package main
    
        import "fmt"
    
        func main() {
            q := []int{2, 3, 5, 7, 11, 13}
            fmt.Println(q)
    
            r := []bool{true, false, true, true, false, true}
            fmt.Println(r)
    
            s := []struct {
                i int
                b bool
            }{
                {2, true},
                {3, false},
                {5, true},
                {7, true},
                {11, false},
                {13, true},
            }
            fmt.Println(s)
        }
    

    Output:

      [2 3 5 7 11 13]
      [true false true true false true]
      [{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]
    
  • Slice default: Khi "cắt lát" một slice, bạn có thể bỏ qua giới hạn thấp hoặc giới hạn cao. Giá trị default của giới hạn thấp là 0, cao là đội dài của slice.
    Ví dụ ta có array: var a [10]int Các biểu thức slice dưới đây là tương đương nhau:

    a[0:10]
    a[:10]
    a[0:]
    a[:]
    

    Ví dụ "cắt lát" slice:

    package main
    
    import "fmt"
    
    func main() {
        s := []int{2, 3, 5, 7, 11, 13}
    
        s = s[1:4]
        fmt.Println(s)  // [3 5 7]
    
        s = s[:2]
        fmt.Println(s)  // [3 5]
    
        s = s[1:]
        fmt.Println(s)  // [5]
    }
    
  • Slice lengthcapacity: Một slice luôn có 2 thành phần là độ dài(length) và sức chứa(capacity):

    • Độ dài của một lát là số phần tử mà nó chứa.
    • Sức chứa của một lát là số phần tử trong mảng cơ sở, tính từ phần tử đầu tiên trong lát.
    • Độ dài và sức chứa của một lát s có thể được lấy bằng cách sử dụng các biểu thức len(s) và cap(s)
    • Bạn có thể mở rộng độ dài của một slice bằng cách cắt lại (re-slice) nó, miễn là nó có đủ sức chứa.
    package main
    
    import "fmt"
    
    func main() {
        s := []int{2, 3, 5, 7, 11, 13}
        printSlice(s)
    
        // Slice the slice to give it zero length.
        s = s[:0]
        printSlice(s)
    
        // Extend its length.
        s = s[:4]
        printSlice(s)
    
        // Drop its first two values.
        s = s[2:]
        printSlice(s)
    }
    
    func printSlice(s []int) {
        fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    }
    

    Ouput:

    len=6 cap=6 [2 3 5 7 11 13]
    len=0 cap=6 []
    len=4 cap=6 [2 3 5 7]
    len=2 cap=4 [5 7]
    
  • Nil slice: Giá trị default của một slice là nil, độ dài = 0 và sức chứa = 0

  • Tạo slice bằng make: Slice có thể được tạo bằng việc dùng built-in make function, đây là cách bạn tạo các mảng có kích thước động.

    a := make([]int, 5)  // len(a)=5
    
    b := make([]int, 0, 5) // len(b)=0, cap(b)=5
    b = b[:cap(b)] // len(b)=5, cap(b)=5
    b = b[1:]      // len(b)=4, cap(b)=4
    
  • Loop slice: Có thể dùng for range để loop slice, index sẽ được đánh từ 0 đến độ dài của slice

    package main
    
    import "fmt"
    
    var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
    
    func main() {
        for i, v := range pow {
            fmt.Printf("2 x %d = %d\n", i, v)
        }
    }
    

    Output:

    2 x 0 = 1
    2 x 1 = 2
    2 x 2 = 4
    2 x 3 = 8
    2 x 4 = 16
    2 x 5 = 32
    2 x 6 = 64
    2 x 7 = 128
    

Maps

  • Map là một kiểu dữ liệu dạng bản đồ (associative array) được sử dụng để ánh xạ các khóa (keys) tới các giá trị (values). Mỗi khóa trong một bản đồ phải là duy nhất và không thể thay đổi.
  • Cú pháp khai báo map: var myMap map[KeyType]ValueType
    • KeyType: Kiểu dữ liệu của các khóa
    • ValueType: Kiểu dữ liệu của giá trị.
      Ví dụ: ar ages map[string]int // Một map có key là chuỗi và value là số nguyên
  • Một map cũng có thể được khởi tạo bằng hàm make, nhưng việc này thường không cần thiết nếu bạn chỉ muốn khởi tạo một map rỗng.
    ages := make(map[string]int)
  • Thêm và truy cập phần tử:
    • Để thêm một phần tử mới vào map, bạn có thể sử dụng cú pháp:
    ages["Alice"] = 30
    
    • Để truy cập một giá trị dựa trên một khóa, bạn có thể sử dụng cú pháp:
    fmt.Println("Tuổi của Alice là:", ages["Alice"])
    
  • Kiểm tra sự tồn tại của key: Bạn có thể kiểm tra xem một khóa có tồn tại trong map hay không bằng cách sử dụng hai giá trị trả về.
    age, ok := ages["Bob"]
    if ok {
        fmt.Println("Tuổi của Bob là:", age)
    } else {
        fmt.Println("Không tìm thấy tuổi của Bob")
    }
    
  • Xóa phần tử: Để xóa một phần tử khỏi map, bạn có thể sử dụng hàm delete()
    delete(ages, "Alice")
    
    Một ví dụ tổng quan:
    package main
    
    import "fmt"
    
    func main() {
        // Khởi tạo một map
        ages := make(map[string]int)
    
        // Thêm các phần tử vào map
        ages["Alice"] = 30
        ages["Bob"] = 35
    
        // Truy cập và in ra giá trị của một phần tử
        fmt.Println("Tuổi của Alice là:", ages["Alice"])
    
        // Kiểm tra sự tồn tại của một khóa
        age, ok := ages["Bob"]
        if ok {
            fmt.Println("Tuổi của Bob là:", age)
        } else {
            fmt.Println("Không tìm thấy tuổi của Bob")
        }
    
        // Xóa một phần tử khỏi map
        delete(ages, "Alice")
    
        // In ra chiều dài của map
        fmt.Println("Số lượng phần tử trong map:", len(ages))
    }
    
    Output:
    Tuổi của Alice là: 30
    Tuổi của Bob là: 35
    Số lượng phần tử trong map: 1
    

Kết bài

Kết thúc một bài khá dài 😁
Trong bài thứ 4 này mình đã giới thiệu với các bạn về các types: pointer, structs, slices, and maps, chúng ta sẽ sử dụng rất rất nhiều những type này trong dự án thực tế đó 😄 Bài tiếp theo mình sẽ giới thiệu về Methods vaf interfaces trong Golang. Nếu có thắc mắc hoặc phát hiện bài viết có thiếu sót gì, hãy comment cho mình biết để mình kịp thời chỉnh sửa cũng như cùng thảo luận về thắc mắc đó nhé 😉 Hi vọng những kiến thức này sẽ giúp ích được các bạn trong hành trình chinh phục Golang. Hẹn gặp lại ở những bài viết tiếp theo trong series Golang Essentials: Nền Tảng và Kiến Thức Cơ Bản. See ya!

Tham khảo: https://go.dev/tour/moretypes


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí