Pointers


1. Pointers là gì?

  • Pointer (con trỏ) trong Go là một biến lưu trữ địa chỉ bộ nhớ của một giá trị thay vì chính giá trị đó.

  • Go hỗ trợ pointer nhưng với cách tiếp cận đơn giản hơn so với C/C++, loại bỏ các phép toán con trỏ phức tạp (như cộng trừ địa chỉ).

  • Pointer được sử dụng để:

    • Thay đổi giá trị của biến qua tham chiếu.

    • Tránh sao chép dữ liệu lớn để tối ưu hiệu năng.

    • Quản lý tài nguyên (như kết nối mạng, file) hoặc trạng thái trong concurrency.

Cú pháp cơ bản:

  • &x: Lấy địa chỉ của biến x.

  • *T: Kiểu con trỏ trỏ tới giá trị kiểu T.

  • *p: Giải tham chiếu (dereference) để lấy giá trị tại địa chỉ mà p trỏ tới.

Ví dụ cơ bản:

func main() {
    x := 10
    p := &x       // p là con trỏ trỏ tới x
    fmt.Println(*p) // Output: 10 (giá trị tại địa chỉ p)
    *p = 20       // Thay đổi giá trị tại địa chỉ p
    fmt.Println(x) // Output: 20 (x đã thay đổi)
}

2. Tại sao cần Pointers?

  • Thay đổi giá trị: Go là pass-by-value, nghĩa là khi truyền biến vào hàm, một bản sao được tạo ra. Để thay đổi giá trị gốc, cần dùng pointer.

  • Tối ưu hiệu năng: Với dữ liệu lớn (như struct phức tạp), truyền pointer tránh sao chép toàn bộ dữ liệu.

  • Trạng thái chia sẻ: Trong concurrency, pointer giúp nhiều goroutine truy cập và cập nhật cùng một dữ liệu (với cơ chế đồng bộ như mutex).

Ví dụ thay đổi giá trị:

func updateValue(x *int) {
    *x = 100
}

func main() {
    x := 10
    updateValue(&x)
    fmt.Println(x) // Output: 100
}

3. Khai báo và khởi tạo Pointer

  • Khai báo pointer:

    var p *int // p là con trỏ trỏ tới int, mặc định là nil
    fmt.Println(p) // Output: <nil>
  • Khởi tạo pointer:

    • Sử dụng &:

      x := 42
      p := &x
    • Sử dụng new:

      p := new(int) // Tạo con trỏ tới một int với giá trị 0
      *p = 42

Lưu ý:

  • new(T) trả về *T và cấp phát bộ nhớ với giá trị zero của T.

  • Pointer chưa khởi tạo (nil) sẽ gây panic nếu giải tham chiếu (*p).


4. Sử dụng Pointer với Struct

Pointer thường được dùng với struct để:

  • Thay đổi giá trị của struct.

  • Tránh sao chép struct lớn.

Ví dụ:

type Person struct {
    Name string
    Age  int
}

func updatePerson(p *Person) {
    p.Name = "Alice"
    p.Age = 30
}

func main() {
    p := Person{Name: "Bob", Age: 20}
    updatePerson(&p)
    fmt.Println(p) // Output: {Alice 30}
}

So sánh pass-by-value và pass-by-pointer:

func updateNoPointer(p Person) {
    p.Name = "Charlie"
}

func main() {
    p := Person{Name: "Bob", Age: 20}
    updateNoPointer(p)
    fmt.Println(p) // Output: {Bob 20} (không thay đổi)
}

Mẹo: Với struct lớn, luôn cân nhắc dùng *Struct để tránh sao chép.


5. Pointer Receiver trong Method

Trong Go, method có thể dùng pointer receiver (*T) hoặc value receiver (T). Pointer receiver được dùng khi:

  • Cần thay đổi trạng thái của struct.

  • Tránh sao chép struct lớn để tăng hiệu năng.

Ví dụ:

type Counter struct {
    Value int
}

func (c *Counter) Increment() {
    c.Value++
}

func (c Counter) Get() int {
    return c.Value
}

func main() {
    c := Counter{Value: 10}
    c.Increment()
    fmt.Println(c.Get()) // Output: 11
}

Khi nào dùng Pointer Receiver?

  • Thay đổi trạng thái: Dùng *T.

  • Struct lớn: Dùng *T để tránh sao chép.

  • Chỉ đọc: Dùng T nếu struct nhỏ (như int, bool) hoặc không cần thay đổi.

  • Consistency: Nếu một method dùng *T, tất cả các method của struct nên dùng *T để tránh nhầm lẫn.

Lưu ý: Go tự động xử lý p.Method() thành (*p).Method() nếu p là con trỏ, và ngược lại.


6. Pointer và Nil

  • Một pointer có giá trị nil nếu chưa được khởi tạo hoặc được gán bằng nil.

  • Gọi method hoặc giải tham chiếu trên nil pointer sẽ gây panic.

Ví dụ lỗi:

func main() {
    var p *int
    fmt.Println(*p) // Panic: invalid memory address or nil pointer dereference
}

Cách tránh:

  • Luôn kiểm tra nil trước khi giải tham chiếu:

    if p != nil {
        fmt.Println(*p)
    }

Nil Receiver:

  • Một số method với pointer receiver có thể xử lý nil an toàn:

    func (p *Person) SayHello() string {
        if p == nil {
            return "Hello from nil"
        }
        return "Hello from " + p.Name
    }
    
    func main() {
        var p *Person
        fmt.Println(p.SayHello()) // Output: Hello from nil
    }

7. Pointer và Slice/Map

  • Slicemap trong Go là các kiểu tham chiếu (reference types), nghĩa là chúng đã chứa một con trỏ bên trong. Tuy nhiên, đôi khi bạn vẫn cần dùng pointer để thay đổi chính slice/map.

Ví dụ với Slice:

func appendSlice(s *[]int) {
    *s = append(*s, 100)
}

func main() {
    s := []int{1, 2, 3}
    appendSlice(&s)
    fmt.Println(s) // Output: [1 2 3 100]
}

Tại sao cần *[]int?

  • Nếu chỉ truyền []int, hàm sẽ nhận một bản sao của slice header (chứa con trỏ tới array, length, capacity). Thay đổi array bên trong có thể ảnh hưởng đến slice gốc, nhưng thay đổi length/capacity không được lưu lại.

Map tương tự:

func addToMap(m *map[string]int, key string, value int) {
    if *m == nil {
        *m = make(map[string]int)
    }
    (*m)[key] = value
}

8. Pointer và Concurrency

Pointer thường được dùng trong concurrency để chia sẻ trạng thái giữa các goroutine, nhưng cần cẩn thận với data race.

Ví dụ sai:

type Counter struct {
    Value int
}

func increment(c *Counter) {
    c.Value++ // Data race nếu nhiều goroutine gọi cùng lúc
}

func main() {
    c := &Counter{}
    for i := 0; i < 1000; i++ {
        go increment(c)
    }
    time.Sleep(time.Second)
    fmt.Println(c.Value) // Kết quả không dự đoán được
}

Cách sửa: Sử dụng mutex để bảo vệ truy cập:

type Counter struct {
    Value int
    mu    sync.Mutex
}

func increment(c *Counter) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.Value++
}

Mẹo:

  • Chỉ dùng pointer để chia sẻ dữ liệu khi thực sự cần.

  • Cân nhắc sử dụng channel thay vì pointer + mutex nếu có thể, vì channel an toàn hơn và rõ ràng hơn.


9. Tối ưu hóa với Pointer

Một Senior Golang developer luôn chú ý đến hiệu năng và tính an toàn khi dùng pointer:

9.1. Tránh sao chép dữ liệu lớn

  • Với struct lớn, luôn truyền *Struct thay vì Struct.

Ví dụ:

type BigData struct {
    Data [1000]int
}

func process(b *BigData) {
    // Làm gì đó
}

9.2. Tránh lạm dụng Pointer

  • Với kiểu dữ liệu nhỏ (int, bool, struct nhỏ), không cần dùng pointer vì sao chép nhanh hơn và an toàn hơn.

  • Ví dụ: *int thường không cần thiết trừ khi bạn cần thay đổi giá trị gốc.

9.3. Memory Alignment

  • Khi dùng pointer trong struct, chú ý đến thứ tự trường để tránh lãng phí bộ nhớ do padding:

    type Optimized struct {
        p *int64 // 8 bytes
        b bool   // 1 byte
    }

9.4. Escape Analysis

  • Go có cơ chế escape analysis để quyết định xem một biến nên được cấp phát trên stack hay heap.

  • Truyền pointer có thể khiến biến "escape" lên heap, tăng áp lực cho garbage collector.

Ví dụ:

func createPointer() *int {
    x := 42
    return &x // x escape lên heap
}

Mẹo: Hiểu escape analysis bằng cách chạy go build -gcflags="-m".


10. Pattern nâng cao với Pointer

10.1. Builder Pattern

Pointer được dùng để xây dựng đối tượng phức tạp một cách linh hoạt:

type Config struct {
    Host string
    Port int
}

type ConfigBuilder struct {
    config *Config
}

func NewConfigBuilder() *ConfigBuilder {
    return &ConfigBuilder{config: &Config{}}
}

func (b *ConfigBuilder) WithHost(host string) *ConfigBuilder {
    b.config.Host = host
    return b
}

func (b *ConfigBuilder) WithPort(port int) *ConfigBuilder {
    b.config.Port = port
    return b
}

func (b *ConfigBuilder) Build() *Config {
    return b.config
}

10.2. Pool Pattern

Pointer được dùng để tái sử dụng tài nguyên:

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func getBuffer() *bytes.Buffer {
    return bufferPool.Get().(*bytes.Buffer)
}

func putBuffer(b *bytes.Buffer) {
    b.Reset()
    bufferPool.Put(b)
}

11. Những lỗi thường gặp

  1. Nil pointer dereference: Gọi *p khi pnil.

  2. Data race: Nhiều goroutine truy cập pointer mà không có đồng bộ.

  3. Lạm dụng pointer: Dùng *T khi không cần, gây phức tạp mã.

  4. Memory leak: Giữ pointer tới dữ liệu không còn cần, ngăn garbage collection.

  5. Sao chép slice/map không mong muốn: Thay đổi slice/map gốc khi chỉ muốn làm việc với bản sao.


12. Ứng dụng thực tế

  • API Server: Pointer để cập nhật trạng thái request/response.

  • Database: Pointer để quản lý kết nối hoặc ánh xạ dữ liệu (như GORM).

  • Concurrency: Pointer để chia sẻ trạng thái giữa goroutine.

  • Performance-critical code: Pointer để tránh sao chép struct lớn.

Ví dụ thực tế:

type User struct {
    ID   int
    Name string
}

func updateUser(db *sql.DB, user *User) error {
    _, err := db.Exec("UPDATE users SET name = ? WHERE id = ?", user.Name, user.ID)
    return err
}

14. Tài liệu tham khảo


Last updated