Go-lang: Anonymous fields in structs

Go-lang cho phép chúng ta định nghĩa 1 struct theo cách rất thông thường như ngôn ngữ C/C++. Tuy nhiên Go-lang còn cho phép định nghĩa các field trong struct nhưng không cần có tên gọi, chỉ cần kiểu. Các fields này được gọi là anonymous fields (Các thuộc tính/trường nặc, vô danh). Trong bài viết này chúng ta cùng xem xét 1 số ví dụ để thấy tính năng này hữu ích thế nào nhé.

Trong ví dụ bên dưới, tôi định nghĩa Kitchen struct chỉ gồm duy nhất 1 trường numOfPlates, ngoài ra tôi còn thể định nghĩa thêm 1 struct là House (chứa Kitchen như là 1 trường trong struct này). Hãy để ý, ta có thể thấy cách mà House struct định nghĩa Kitchen rất đặc biệt, không có bất cứ định danh (name) nào cho thuộc tính có kiểu Kitchen này.

package main

import "fmt"

type Kitchen struct {
    numOfPlates int 
}

type House struct {
    Kitchen //anonymous field
    numOfRooms int 
}

func main() {
    h := House{Kitchen{10}, 3} //to initialize you have to use composed type name.
    fmt.Println("House h has this many rooms:", h.numOfRooms) //numOfRooms is a field of House
    fmt.Println("House h has this many plates:", h.numOfPlates) //numOfPlates is a field of anonymous field Kitchen, so it can be referred to like a field of House
    fmt.Println("The Kitchen contents of this house are:", h.Kitchen) //we can refer to the embedded struct in its entirety by referring to the name of the struct type
}

Và đây là kết quả khi chạy chương trình trên:

House h has this many rooms: 3
House h has this many plates: 10
The Kitchen contents of this house are: {10}

Trường hợp này, Kitchen xuất hiện và được gọi với cái tên rất lạ: anonymous field (trường nặc danh). Có 3 điều quan trọng được chú ý tới là

  • Việc định nghĩa này cho phép chúng ta access tới các member của Kitchen như các member bên trong House struct, rất tiện lợi.
  • Composed field cũng có thể được access thông qua tên kiểu dữ liệu của chúng. Do đó trong trường hợp này anonymous field cho Kitchen có thể được access với cú pháp h.Kitchen, nếu bạn số lượng đĩa trong nhà bếp, ta có thể implement như sau: fmt.Println(h.Kitchen.numOfPlates).
  • Chúng ta có thể sử dụng tên kiểu dữ liệu khi khởi tạo giá trị cho 1 biến: h := House{Kitchen{10}, 3}. Việc khởi tạo này yêu cầu chúng ta phải khai báo chính xác kiểu dữ liệu và giá trị tương ứng bên trong {}, do đó nếu chỉ gán như sau: h := House{{10}, 3} hoặc h := House{10, 3} sẽ xảy ra lỗi khi compile.

Conflict name khi khai báo anonymous field

Điều gì sẽ xảy ra khi các composed structs có field trùng tên ? Theo lý thuyết, nếu có 1 field trong outer struct có cùng tên với 1 field ở trong inner anonymous struct thì mặc định khi ta truy cập tới field đó sẽ nhận được giá trị của field trong outer struct. Trong ví dụ bên dưới đây, cả KitchenHouse đều có chứa numOfLamps field, nhưng do House là outer struct, nên numOfLamps của House sẽ được gán mặc định thay cho giá trị của Kitchen. Nếu như bạn vẫn muốn access tới numOfLamps của Kitchen, thì có thể tham chiếu thông qua tên kiểu dữ liệu: h.Kitchen.numOfLamps.

package main

import "fmt"

type Kitchen struct {
    numOfLamps int
}

type House struct {
    Kitchen
    numOfLamps int
}

func main() {
    h := House{Kitchen{2}, 10} //kitchen has 2 lamps, and the House has a total of 10 lamps
    fmt.Println("House h has this many lamps:", h.numOfLamps) //this is ok - the outer House's numOfLamps hides the other one.  Output is 10.
    fmt.Println("The Kitchen in house h has this many lamps:", h.Kitchen.numOfLamps) //we can still reach the number of lamps in the kitchen by using the type name h.Kitchen
}

Và đây là kết quả khi chạy chương trình trên:

House h has this many lamps: 10
The Kitchen in house h has this many lamps: 2

Dễ dàng nhận thấy chúng ta có 1 rule đối với field trong trường hợp trùng tên ở các cấp (level) khác nhau trong composition. Nhưng không biết có rule nào trong trường hợp các field bị trùng tên mà lại cùng level trong composition struct không nhỉ ? Câu trả lời là không, điều đó có nghĩa là bạn sẽ phải tự khắc phục tình trạng đó nếu xảy ra.

Trong đoạn code dưới đây, cả KitchenBedroom đều có trường số lượng đèn (numOfLamps), và cả 2 đều là anonymous field bên trong House. Bây giờ nếu chúng ta tham chiếu tới House.numOfLamps, Go compiler không thể tự giải quyết được vấn đề này, chương trình không thể xác định bạn muốn tham chiếu tới numOfLamps bên trong Kitchen hay Bedroom ?, nó sẽ throw ra lỗi.

package main

import "fmt"

type Kitchen struct {
    numOfLamps int
}

type Bedroom struct {
    numOfLamps int
}

type House struct {
    Kitchen
    Bedroom
}

func main() {
    h := House{Kitchen{2}, Bedroom{3}} //kitchen has 2 lamps, Bedroom has 3 lamps
    fmt.Println("Ambiguous number of lamps:", h.numOfLamps) //this is an error due to ambiguousness - is it Kitchen.numOfLamps or Bedroom.numOfLamps
}

Compiler error 😃)

main.go:20: ambiguous selector h.numOfLamps

Để giải quyết vấn đề này, chúng ta phải refer tới field thông qua kiểu dữ liệu của anonymous field. Hãy xem ví dụ bên dưới:

package main

import "fmt"

type Kitchen struct {
    numOfLamps int
}

type Bedroom struct {
    numOfLamps int
}

type House struct {
    Kitchen
    Bedroom
}

func main() {
    h := House{Kitchen{2}, Bedroom{3}}
    fmt.Println("House h has this many lamps:", h.Kitchen.numOfLamps + h.Bedroom.numOfLamps) //refer to fields via type name
}

Và đây là kết quả:

House h has this many lamps: 5

Bạn có thể chạy thử code tại đây: https://play.golang.org/p/0ltbGsw7h1

Embeded struct (Anonymous field) được ứng dụng rất nhiều trong Go-lang, cho thấy tầm quan trọng của nó. Chắc hẳn nếu bạn thường xuyên làm việc với xử lý concurrency, đặc biệt khi thực hiện các thao tác đọc ghi dữ liệu song song, rất hay bắt gặp đoạn code sau:

var hits struct {
    sync.Mutex
    n int
}

hits.Lock()
hits.n++
hits.Unlock()

Struct hits được định nghĩa mới, có đầy đủ các method của sync.Mutex struct, đây là 1 cách giúp source code sáng sủa, không bị lặp lại trong Go-lang.

Ở bài viết tiếp theo trong Go-lang series tự thân vận động, tôi sẽ thử mổ xẻ câu lệnh docker stats của Docker Engine thông qua source code được viết bằng Go-lang của Docker tại đây, khi ấy ta sẽ thấy Embeded struct (Anonymous field) được sử dụng rất nhiều và để đọc được code của 1 Project mệt thế nào =))

Tham khảo