Back to blog

Học Go: Method - Pointer Receiver vs Value Receiver

gogolangprogrammingmethods
Học Go: Method - Pointer Receiver vs Value Receiver

Khi viết methods trong Go, một trong những quyết định cơ bản bạn sẽ đưa ra là sử dụng pointer receiver hay value receiver. Lựa chọn này ảnh hưởng đến cách methods tương tác với dữ liệu, ảnh hưởng đến cả hiệu suất và hành vi.

Methods Trong Go Là Gì?

Trong Go, một method là một function với một receiver argument đặc biệt. Receiver xuất hiện giữa từ khóa func và tên method:

type Person struct {
    Name string
    Age  int
}
 
// Method với value receiver
func (p Person) GetName() string {
    return p.Name
}
 
// Method với pointer receiver
func (p *Person) SetName(name string) {
    p.Name = name
}

Value Receiver

Value receiver tạo ra một bản sao của struct khi method được gọi. Bất kỳ sửa đổi nào được thực hiện bên trong method đều không ảnh hưởng đến struct gốc.

type Counter struct {
    Count int
}
 
func (c Counter) Increment() {
    c.Count++ // Điều này chỉ sửa đổi bản sao!
}
 
func main() {
    counter := Counter{Count: 0}
    counter.Increment()
    fmt.Println(counter.Count) // Output: 0 (không thay đổi!)
}

Khi Nào Sử Dụng Value Receivers

  1. Method không sửa đổi receiver - Khi bạn chỉ cần đọc dữ liệu
  2. Structs nhỏ - Copy rẻ cho các cấu trúc dữ liệu nhỏ
  3. Tính bất biến - Khi bạn muốn đảm bảo dữ liệu gốc không thay đổi
  4. Simple types - Cho các built-in types như int, string, v.v.
type Point struct {
    X, Y int
}
 
// Sử dụng value receiver tốt - chỉ đọc giá trị
func (p Point) Distance() float64 {
    return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}

Pointer Receiver

Pointer receiver truyền một tham chiếu đến struct. Method có thể sửa đổi struct gốc, và không có copy nào xảy ra.

type Counter struct {
    Count int
}
 
func (c *Counter) Increment() {
    c.Count++ // Điều này sửa đổi bản gốc!
}
 
func main() {
    counter := Counter{Count: 0}
    counter.Increment()
    fmt.Println(counter.Count) // Output: 1 (đã sửa đổi!)
}

Khi Nào Sử Dụng Pointer Receivers

  1. Method sửa đổi receiver - Khi bạn cần thay đổi state của struct
  2. Structs lớn - Tránh các copy operations tốn kém
  3. Tính nhất quán - Nếu một số methods cần pointers, hãy sử dụng pointers cho tất cả methods của type đó
  4. Mutexes hoặc sync primitives khác - Những thứ này không được copy
type Account struct {
    Balance float64
    mu      sync.Mutex
}
 
// Phải sử dụng pointer receiver - sửa đổi state và chứa mutex
func (a *Account) Deposit(amount float64) {
    a.mu.Lock()
    defer a.mu.Unlock()
    a.Balance += amount
}
 
func (a *Account) GetBalance() float64 {
    a.mu.Lock()
    defer a.mu.Unlock()
    return a.Balance
}

Sự Khác Biệt Chính

Khía cạnhValue ReceiverPointer Receiver
Sửa đổiKhông thể sửa đổi bản gốcCó thể sửa đổi bản gốc
Bộ nhớTạo bản saoSử dụng tham chiếu
Hiệu suấtChậm hơn với structs lớnNhanh hơn với structs lớn
nil safetyKhông thể gọi trên nilCó thể gọi trên nil
Thread safetyAn toàn hơn (immutable)Yêu cầu synchronization

Lỗi Quan Trọng: nil Pointer Receivers

Pointer receivers có thể được gọi ngay cả khi receiver là nil. Bạn cần xử lý điều này một cách rõ ràng:

type Tree struct {
    Value int
    Left  *Tree
    Right *Tree
}
 
func (t *Tree) Insert(value int) {
    if t == nil {
        // Không thể sửa đổi nil receiver!
        // Đây là một lỗi phổ biến
        return
    }
    // ... logic insertion
}

Best Practices

  1. Nhất quán - Nếu bất kỳ method nào cần pointer receiver, hãy sử dụng pointer receivers cho tất cả methods của type đó
  2. Xem xét semantics - Type của bạn đại diện cho một giá trị (như time.Time) hay một tài nguyên (như os.File)?
  3. Ghi chú lựa chọn - Làm rõ trong comments liệu một type nên được copy hay referenced
  4. Mặc định là pointer receivers - Khi không chắc chắn, pointer receivers linh hoạt hơn
// Tốt: Pointer receivers nhất quán
type User struct {
    ID   int
    Name string
}
 
func (u *User) SetName(name string) {
    u.Name = name
}
 
func (u *User) GetID() int {
    return u.ID // Vẫn sử dụng pointer receiver cho tính nhất quán
}

Ví Dụ Thực Tế

Đây là một ví dụ thực tế cho thấy cả hai cách tiếp cận:

package main
 
import "fmt"
 
// Value type - immutable point
type Point struct {
    X, Y float64
}
 
// Value receiver - trả về Point mới
func (p Point) Add(other Point) Point {
    return Point{X: p.X + other.X, Y: p.Y + other.Y}
}
 
// Reference type - mutable rectangle
type Rectangle struct {
    TopLeft     Point
    BottomRight Point
}
 
// Pointer receiver - sửa đổi tại chỗ
func (r *Rectangle) Move(dx, dy float64) {
    r.TopLeft.X += dx
    r.TopLeft.Y += dy
    r.BottomRight.X += dx
    r.BottomRight.Y += dy
}
 
// Pointer receiver - tính diện tích (nhất quán với Move)
func (r *Rectangle) Area() float64 {
    width := r.BottomRight.X - r.TopLeft.X
    height := r.BottomRight.Y - r.TopLeft.Y
    return width * height
}
 
func main() {
    p1 := Point{X: 1, Y: 2}
    p2 := Point{X: 3, Y: 4}
    p3 := p1.Add(p2)
    fmt.Printf("Sum: (%v, %v)\n", p3.X, p3.Y)
 
    rect := Rectangle{
        TopLeft:     Point{X: 0, Y: 0},
        BottomRight: Point{X: 10, Y: 10},
    }
    fmt.Printf("Area: %v\n", rect.Area())
    rect.Move(5, 5)
    fmt.Printf("After move: %+v\n", rect)
}

Kết Luận

Việc lựa chọn giữa pointer và value receivers là về việc hiểu semantics của dữ liệu:

  • Sử dụng value receivers cho các types nhỏ, bất biến mà việc copy là rẻ
  • Sử dụng pointer receivers cho các types cần được sửa đổi, structs lớn, hoặc khi tính nhất quán yêu cầu

Khi nghi ngờ, hãy ưu tiên pointer receivers—chúng linh hoạt hơn và ngăn chặn các lỗi phổ biến liên quan đến việc copy không chủ ý.

Happy coding in Go! 🚀

Tài Liệu Tham Khảo

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.