Menu

Biến trong Golang: var, :=, giá trị zero và shadowing

Cách khai báo biến trong Go với var và dạng ngắn :=, giá trị zero là gì, gán nhiều biến cùng lúc, cùng các lỗi shadowing và biến không dùng đến khiến người mới học Go hay vấp.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Go có hai cách khai báo biến (variable): var, dùng được ở mọi nơi, và :=, dạng ngắn chỉ dùng trong hàm và tự suy ra kiểu.

Mỗi biến có một kiểu cố định được quyết định lúc biên dịch. Khi bạn cho giá trị, Go suy ra kiểu từ giá trị đó, nên viết thêm kiểu (var city string = "Lisbon") vẫn hợp lệ nhưng thừa.

var và :=

var:=
Dùng ở đâucấp package và trong hàmchỉ trong hàm
Kiểutùy chọn; bắt buộc khi không có giá trịluôn được suy ra
Giá trị ban đầutùy chọn (nếu không thì là giá trị zero)bắt buộc
Khai báo lạikhông bao giờđược, nếu ít nhất một biến bên trái là biến mới

Lựa chọn chuẩn trong một hàm:

  • x := value khi bạn có giá trị khởi đầu.
  • var x T khi bạn muốn giá trị zero, như một cách nói "cố ý để trống": var buf bytes.Buffer, var total int.
  • var x T = value khi kiểu được suy ra không phải kiểu bạn muốn, ví dụ var ratio float64 = 1 (viết ratio := 1 sẽ ra int).

Dùng := bên ngoài hàm là lỗi cú pháp:

./main.go:5:1: syntax error: non-declaration statement outside function body

Giá trị zero

Biến khai báo không kèm giá trị không bao giờ chứa rác. Nó giữ giá trị zero của kiểu:

KiểuGiá trị zero
số nguyên, số thực0
boolfalse
string""
con trỏ, slice, map, channel, hàm, interfacenil
mảng, structmọi phần tử hoặc trường đều mang giá trị zero

Các API Go tốt được thiết kế để giá trị zero dùng được ngay: sync.Mutex zero đang ở trạng thái mở khóa, bytes.Buffer zero rỗng và sẵn sàng, còn len, range và append đều chạy được trên slice nil. Thứ cần để ý là map: đọc map nil trả về giá trị zero, nhưng ghi vào nó sẽ gây panic. Hãy tạo map bằng make hoặc literal trước khi ghi dữ liệu vào.

Khai báo nhiều biến

Khối var ( ... ) gom các biến cấp package có liên quan lại với nhau. Trong phép gán nhiều biến như x, y = y, x, mọi biểu thức bên phải được tính xong trước khi gán bất cứ thứ gì, nhờ vậy phép hoán đổi mới chạy đúng.

:= với nhiều tên không khai báo lại gì cả, miễn là ít nhất một tên bên trái là mới. Các tên đã có trong cùng phạm vi chỉ đơn giản được gán giá trị. Nhờ thế bạn dùng lại được err:

f, err := os.Open("a.txt")   // declares f and err
g, err := os.Open("b.txt")   // declares g, assigns to the existing err

Nếu không có tên nào mới, đó là lỗi:

./main.go:7:4: no new variables on left side of :=

Dùng = thường để gán cho biến đã tồn tại.

Biến không dùng đến là lỗi

Một biến cục bộ được khai báo nhưng không bao giờ được đọc sẽ làm build thất bại:

./main.go:9:2: declared and not used: x

Đây không phải cảnh báo mà bạn tắt đi được. Hãy xóa biến, dùng nó, hoặc khi đang gỡ lỗi thì làm nó im bằng định danh trống: _ = x. Định danh trống còn dùng để bỏ đi các giá trị trả về bạn không cần: _, err := fmt.Println("hi").

Biến cấp package và tham số hàm không bị áp quy tắc này.

Phạm vi và shadowing

Một biến tồn tại từ chỗ khai báo đến hết khối bao quanh nó ({ ... }). Một := trong khối bên trong tạo ra một biến mới, kể cả khi bên ngoài đã có biến cùng tên. Biến bên ngoài bị che khuất (shadowing), chứ không bị thay đổi:

Chương trình in ra inside: 42 rồi outside: 0. := bên trong if đã khai báo cả count lẫn err là biến mới trong khối bên trong, vì ở đó chưa có biến nào trong hai biến này. Code biên dịch không một lỗi nào, và đây là nguồn gốc kinh điển của những bug kiểu "giá trị của mình biến đâu mất".

Cách sửa là khai báo err trước rồi gán bằng =:

var err error
count, err = strconv.Atoi(input)

Mặc định go vet không báo shadowing. Bộ phân tích shadow riêng thì có, và các linter như golangci-lint có thể bật nó lên.

Đặt tên

  • Dùng camelCase: maxRetries, userID. Không dùng max_retries.
  • Giữ chữ viết tắt ở cùng một kiểu chữ: userID, httpClient, parseURL, không phải userId.
  • Tên ngắn cho phạm vi ngắn: i trong vòng lặp, r cho một reader, err cho lỗi. Tên dài, mô tả rõ hơn cho biến cấp package.
  • Tên viết hoa chữ cái đầu ở cấp package (var Timeout) được export sang các package khác. Xem trang package và import.

Biến cấp package

Biến khai báo bên ngoài hàm tồn tại suốt chương trình và được khởi tạo trước khi main chạy, theo thứ tự phụ thuộc. Chúng tiện cho cấu hình và cache, và được mọi goroutine dùng chung. Nếu có nhiều hơn một goroutine ghi vào cùng một biến, hãy bảo vệ nó bằng mutex hoặc dùng sync/atomic, nếu không bạn sẽ gặp data race.

Với những giá trị không bao giờ đổi, nên dùng hằng số. Một const không thể bị sửa nhầm và không tốn gì lúc chạy.

Câu hỏi thường gặp

var và := trong Go khác nhau thế nào?

:= khai báo và khởi tạo biến với kiểu được suy ra, và chỉ dùng được bên trong hàm: count := 10. var dùng được ở mọi nơi (kể cả cấp package), cho phép ghi rõ kiểu, và có thể bỏ giá trị ban đầu để nhận giá trị zero: var count int. Trong hàm, cách viết chuẩn của Go là dùng := khi có giá trị ban đầu và var khi bạn muốn giá trị zero.

Giá trị zero trong Go là gì?

Mọi biến khai báo không kèm giá trị đều nhận giá trị zero của kiểu: 0 cho số, false cho bool, "" cho string, và nil cho con trỏ, slice, map, channel, hàm và interface. Struct nhận giá trị zero cho từng trường. Go không có biến chưa được khởi tạo.

Sửa lỗi "declared and not used" trong Go như thế nào?

Go từ chối biến cục bộ được gán nhưng không bao giờ được đọc. Hãy dùng nó, xóa nó, hoặc khi đang gỡ lỗi thì gán nó cho định danh trống bằng _ = x. Biến cấp package và tham số hàm không bị áp quy tắc này.

Khai báo biến toàn cục trong Go như thế nào?

Dùng var ở cấp package, bên ngoài mọi hàm: var counter int. Dạng ngắn := không được phép ở đó. Tên viết hoa chữ cái đầu (var Counter int) thì package khác thấy được; tên viết thường chỉ thấy trong package của nó. Nếu có goroutine ghi vào biến cấp package, hãy bảo vệ nó bằng mutex.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu