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
type
vàstruct
.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
: 0string
: ""bool
: falsepointer
: nilslice/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ớip.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
Sao chép struct lớn: Gây tốn bộ nhớ và CPU.
Quên mutex trong concurrency: Dẫn đến data race.
Sử dụng zero value không mong muốn: Ví dụ, quên khởi tạo con trỏ hoặc slice.
Tag không đúng: Gây lỗi khi encode/decode JSON hoặc mapping database.
Không tối ưu memory alignment: Gây lãng phí bộ nhớ.
15. Tài liệu tham khảo
Thư viện hỗ trợ:
encoding/json
,github.com/go-playground/validator
,gorm.io/gorm
.
Last updated