Structs


1. Structs là gì?

  • Struct (structure) trong Go là một kiểu dữ liệu do người dùng định nghĩa, dùng để nhóm các trường dữ liệu (fields) lại với nhau để biểu diễn một thực thể có ý nghĩa.

  • Struct tương tự như các lớp (class) trong các ngôn ngữ hướng đối tượng, nhưng Go không có khái niệm kế thừa hay đa hình như OOP truyền thống.

Ví dụ cơ bản:

type Person struct {
    Name string
    Age  int
}

func main() {
    // Khởi tạo struct
    p := Person{
        Name: "Alice",
        Age:  30,
    }
    fmt.Println(p.Name) // Output: Alice
}

2. Cú pháp và cách khai báo Structs

  • Struct được khai báo bằng từ khóa typestruct.

  • Các trường (fields) có thể thuộc bất kỳ kiểu dữ liệu nào (bao gồm cả struct khác, con trỏ, slice, map, v.v.).

Cú pháp:

type <TênStruct> struct {
    <TênTrường1> <KiểuDữLiệu1>
    <TênTrường2> <KiểuDữLiệu2>
    // ...
}

Ví dụ với các kiểu dữ liệu khác nhau:

type Employee struct {
    ID        int
    Name      string
    Salary    float64
    IsActive  bool
    Skills    []string
    Address   *string // Con trỏ
    Manager   Person  // Struct lồng nhau
}

3. Khởi tạo Struct

Có nhiều cách để khởi tạo một struct:

3.1. Khởi tạo trực tiếp với giá trị

p := Person{
    Name: "Bob",
    Age:  25,
}

3.2. Khởi tạo không đầy đủ (zero value)

Nếu không gán giá trị, các trường sẽ nhận giá trị mặc định (zero value):

  • int: 0

  • string: ""

  • bool: false

  • pointer: nil

  • slice/map: nil

var p Person
fmt.Println(p) // Output: {"" 0}

3.3. Sử dụng con trỏ

p := &Person{
    Name: "Charlie",
    Age:  40,
}

3.4. Khởi tạo với new

p := new(Person)
p.Name = "David"
p.Age = 35

Lưu ý: new(Person) trả về một con trỏ (*Person) và tất cả các trường được khởi tạo với zero value.


4. Truy cập và sửa đổi trường của Struct

  • Truy cập trường bằng dấu chấm (.).

  • Nếu là con trỏ, Go tự động giải tham chiếu (*p.Name tương đương với p.Name).

Ví dụ:

p := &Person{Name: "Eve", Age: 28}
p.Age = 29
fmt.Println(p.Age) // Output: 29

5. Struct lồng nhau (Nested Structs)

Struct có thể chứa các struct khác để biểu diễn các mối quan hệ phức tạp.

Ví dụ:

type Address struct {
    Street string
    City   string
}

type Employee struct {
    Name    string
    Address Address
}

func main() {
    emp := Employee{
        Name: "Frank",
        Address: Address{
            Street: "123 Main St",
            City:   "New York",
        },
    }
    fmt.Println(emp.Address.City) // Output: New York
}

6. Anonymous Structs (Struct ẩn danh)

  • Struct ẩn danh được khai báo mà không cần đặt tên, thường dùng cho các trường hợp sử dụng tạm thời.

Ví dụ:

func main() {
    person := struct {
        Name string
        Age  int
    }{
        Name: "Grace",
        Age:  22,
    }
    fmt.Println(person.Name) // Output: Grace
}

Ứng dụng thực tế: Anonymous structs thường được dùng trong JSON unmarshalling hoặc khi trả về dữ liệu tạm thời.


7. Struct với Method

Go hỗ trợ gắn các method vào struct để thêm hành vi, tương tự như các phương thức trong OOP.

Ví dụ:

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    fmt.Println(rect.Area()) // Output: 50
}
  • Receiver: r Rectangle là tham số nhận struct.

  • Pointer Receiver: Nếu muốn thay đổi giá trị của struct, sử dụng con trỏ (*Rectangle).

Ví dụ với Pointer Receiver:

func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    rect.Scale(2)
    fmt.Println(rect) // Output: {20 10}
}

Khi nào dùng Pointer Receiver?

  • Khi cần thay đổi giá trị của struct.

  • Khi struct lớn, tránh sao chép để tối ưu hiệu năng.


8. Tags trong Struct

  • Struct có thể gắn tags để cung cấp metadata, thường dùng trong JSON, database, hoặc các thư viện khác.

Ví dụ:

type User struct {
    Name     string `json:"name"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"-"` // Bỏ qua khi encode JSON
}

func main() {
    user := User{
        Name:     "John",
        Email:    "john@example.com",
        Password: "secret",
    }
    jsonData, _ := json.Marshal(user)
    fmt.Println(string(jsonData)) // Output: {"name":"John","email":"john@example.com"}
}

Ứng dụng thực tế:

  • Tags được dùng nhiều trong:

    • JSON encoding/decoding (encoding/json).

    • Validation (thư viện như github.com/go-playground/validator).

    • ORM (như gorm.io/gorm).


9. So sánh Struct

  • Struct có thể được so sánh bằng toán tử == hoặc != nếu tất cả các trường đều là comparable.

Ví dụ:

type Point struct {
    X int
    Y int
}

func main() {
    p1 := Point{X: 1, Y: 2}
    p2 := Point{X: 1, Y: 2}
    fmt.Println(p1 == p2) // Output: true
}

Lưu ý:

  • Nếu struct chứa các trường không comparable (như slice, map, function), bạn không thể so sánh trực tiếp.

  • Để so sánh phức tạp, hãy viết hàm riêng hoặc dùng thư viện như github.com/google/go-cmp.


10. Struct và Concurrency

Khi sử dụng struct trong môi trường đa luồng (goroutines), bạn cần cẩn thận với data race.

Ví dụ sai:

type Counter struct {
    Value int
}

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

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

Cách sửa: Sử dụng mutex để bảo vệ dữ liệu:

type Counter struct {
    Value int
    mu    sync.Mutex
}

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

11. Tối ưu hóa Struct

Một Senior Golang developer luôn chú ý đến hiệu năng khi thiết kế struct:

11.1. Sắp xếp trường để giảm memory padding

  • Go sắp xếp các trường trong struct để tối ưu hóa bộ nhớ, nhưng thứ tự khai báo có thể gây lãng phí (padding).

Ví dụ không tối ưu:

type BadStruct struct {
    a int8  // 1 byte
    b int64 // 8 bytes
    c int8  // 1 byte
}

Kích thước của BadStruct có thể là 24 bytes do padding.

Cách tối ưu:

type GoodStruct struct {
    b int64 // 8 bytes
    a int8  // 1 byte
    c int8  // 1 byte
}

Kích thước của GoodStruct là 16 bytes.

Mẹo: Sắp xếp các trường từ lớn đến nhỏ (int64, int32, int8, bool, v.v.).

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

  • Nếu struct lớn, hãy truyền con trỏ (*Struct) thay vì giá trị để tránh sao chép tốn tài nguyên.

11.3. Sử dụng embedded struct để tái sử dụng

  • Go hỗ trợ embedding để nhúng một struct vào struct khác, giúp tái sử dụng các trường và phương thức.

Ví dụ:

type Base struct {
    CreatedAt time.Time
}

type Post struct {
    Base      // Embedded struct
    Title     string
    Content   string
}

func main() {
    post := Post{
        Base:    Base{CreatedAt: time.Now()},
        Title:   "Hello",
        Content: "World",
    }
    fmt.Println(post.CreatedAt) // Truy cập trực tiếp
}

12. Ứng dụng thực tế của Struct

Struct được sử dụng rộng rãi trong:

  • API: Biểu diễn request/response (JSON).

  • Database: Map dữ liệu từ SQL/NoSQL (thư viện như GORM).

  • Configuration: Lưu trữ cấu hình ứng dụng.

  • Concurrency: Quản lý trạng thái trong goroutines.

Ví dụ thực tế với API:

type CreateUserRequest struct {
    Username string `json:"username" validate:"required,min=3"`
    Email    string `json:"email" validate:"required,email"`
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid request", http.StatusBadRequest)
        return
    }

    // Validate
    validate := validator.New()
    if err := validate.Struct(req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // Xử lý logic
    w.WriteHeader(http.StatusCreated)
}

13. Những lỗi thường gặp với Struct

  1. Sao chép struct lớn: Gây tốn bộ nhớ và CPU.

  2. Quên mutex trong concurrency: Dẫn đến data race.

  3. Sử dụng zero value không mong muốn: Ví dụ, quên khởi tạo con trỏ hoặc slice.

  4. Tag không đúng: Gây lỗi khi encode/decode JSON hoặc mapping database.

  5. Không tối ưu memory alignment: Gây lãng phí bộ nhớ.


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


Last updated