Variables


1. Tổng quan về Variables trong Go

Biến trong Go là một cách để lưu trữ dữ liệu trong bộ nhớ, được đặt tên và có kiểu dữ liệu cụ thể. Go là một ngôn ngữ statically typed (kiểu tĩnh), nghĩa là kiểu của biến phải được xác định tại thời điểm biên dịch và không thể thay đổi trong thời gian chạy.

Các đặc điểm chính của biến trong Go:

  • Khai báo rõ ràng: Bạn phải khai báo biến với kiểu hoặc để Go suy ra kiểu (type inference).

  • Zero value: Nếu biến không được gán giá trị ban đầu, Go tự động gán giá trị mặc định (zero value) dựa trên kiểu.

  • Scope: Biến có phạm vi rõ ràng (package-level, function-level, block-level).

  • Immutability: Go không có từ khóa const cho biến thông thường, nhưng bạn có thể dùng const cho hằng số.


2. Cách khai báo biến trong Go

Go cung cấp nhiều cách để khai báo biến, và mỗi cách có ngữ cảnh sử dụng riêng.

2.1. Sử dụng từ khóa var

Cú pháp:

var variableName type
var variableName type = initialValue

Ví dụ:

var x int           // Khai báo biến x kiểu int, giá trị mặc định là 0
var y int = 42     // Khai báo và gán giá trị ban đầu
var name string = "Golang" // Biến kiểu string
  • Zero value:

    • int, float64, ...: 0

    • string: "" (chuỗi rỗng)

    • bool: false

    • pointer, slice, map, channel, interface: nil

2.2. Khai báo ngắn gọn với := (Short Variable Declaration)

Cú pháp:

variableName := initialValue

Ví dụ:

x := 42           // Go tự suy ra kiểu là int
name := "Golang"  // Go tự suy ra kiểu là string

Lưu ý:

  • Chỉ sử dụng := trong các block scope (ví dụ: trong hàm).

  • Không sử dụng := ở package-level.

  • := yêu cầu gán giá trị ban đầu.

  • Nếu biến đã tồn tại, := sẽ báo lỗi. Thay vào đó, dùng = để gán lại giá trị.

2.3. Khai báo nhiều biến cùng lúc

Go cho phép khai báo nhiều biến trong một dòng, rất tiện lợi.

Ví dụ:

var (
    x int    = 10
    y string = "hello"
    z bool   = true
)

Hoặc với short declaration:

a, b, c := 1, "two", true

2.4. Khai báo ở Package-level

Biến ở cấp package (ngoài hàm) phải dùng var, không thể dùng :=.

Ví dụ:

package main

import "fmt"

var globalVar string = "I'm global"

func main() {
    fmt.Println(globalVar)
}

3. Kiểu dữ liệu và Type Inference

Go hỗ trợ nhiều kiểu dữ liệu cơ bản (int, string, bool, float64, v.v.) và kiểu phức hợp (slice, map, struct, v.v.). Khi khai báo biến, bạn có thể chỉ định kiểu rõ ràng hoặc để Go suy ra kiểu.

Ví dụ về type inference:

x := 42         // int
y := 3.14       // float64
z := "hello"    // string
w := true       // bool

Chuyên sâu:

  • Type inference không phải là "đoán mò". Go xác định kiểu dựa trên giá trị ban đầu tại thời điểm biên dịch.

  • Nếu bạn cần kiểm soát kiểu chính xác (ví dụ: int32 thay vì int), hãy khai báo kiểu rõ ràng:

    var x int32 = 42

4. Scope và Lifetime của biến

4.1. Scope

  • Package-level: Biến khai báo ngoài hàm có phạm vi toàn package. Có thể được truy cập từ bất kỳ hàm nào trong package (hoặc từ package khác nếu được export - tên bắt đầu bằng chữ in hoa).

  • Function-level: Biến khai báo trong hàm chỉ tồn tại trong hàm đó.

  • Block-level: Biến khai báo trong một block (ví dụ: if, for) chỉ tồn tại trong block đó.

Ví dụ:

package main

import "fmt"

var global = "I'm global"

func main() {
    local := "I'm local"
    fmt.Println(global, local)

    if true {
        blockVar := "I'm in a block"
        fmt.Println(blockVar)
    }
    // fmt.Println(blockVar) // Lỗi: blockVar không tồn tại ngoài block
}

4.2. Lifetime

  • Biến tồn tại trong suốt thời gian scope của nó còn hoạt động.

  • Biến ở package-level tồn tại suốt vòng đời của chương trình.

  • Biến trong hàm/block được garbage collector dọn dẹp khi hàm/block kết thúc.

Chuyên sâu:

  • Go không cho phép shadowing biến trong cùng block nếu không cần thiết. Ví dụ:

    x := 10
    x := 20 // Lỗi: no new variables on left side of :=

    Tuy nhiên, shadowing được phép trong các block con:

    x := 10
    if true {
        x := 20 // Shadowing, x trong block này là biến mới
        fmt.Println(x) // In: 20
    }
    fmt.Println(x) // In: 10

5. Hằng số (Constants)

Hằng số trong Go không phải là biến thông thường, nhưng liên quan mật thiết. Hằng số được khai báo bằng const và có giá trị không thể thay đổi.

Ví dụ:

const Pi float64 = 3.14159
const MaxUsers = 1000 // Untyped constant

Chuyên sâu:

  • Hằng số có thể là untyped (không có kiểu cụ thể) cho đến khi được sử dụng:

    const X = 42 // Untyped
    var y int = X // X tự động trở thành int
  • Hằng số chỉ hỗ trợ các kiểu cơ bản (int, float, string, bool).

  • Go hỗ trợ constant expressions được tính toán tại thời điểm biên dịch:

    const A = 10
    const B = A * 2 // B = 20

6. Best Practices khi làm việc với biến

Một Senior Golang cần hiểu và áp dụng các best practices để viết code sạch và hiệu quả:

  1. Ưu tiên khai báo ngắn gọn (:=) trong hàm:

    • Giảm boilerplate, tăng tính đọc.

    • Ví dụ: x := 42 thay vì var x int = 42.

  2. Khai báo kiểu rõ ràng khi cần thiết:

    • Khi bạn muốn đảm bảo kiểu chính xác (ví dụ: int32 thay vì int).

    • Ví dụ: var x int32 = 42.

  3. Tránh shadowing không cần thiết:

    • Shadowing có thể gây nhầm lẫn. Hãy kiểm tra kỹ các biến trong block con.

  4. Sử dụng zero value hợp lý:

    • Tận dụng zero value thay vì gán giá trị mặc định không cần thiết.

    • Ví dụ: Không cần var x int = 0, vì var x int đã là 0.

  5. Đặt tên biến rõ ràng:

    • Tên biến nên ngắn gọn nhưng mô tả ý nghĩa. Ví dụ: userCount thay vì uc.

    • Biến package-level nên có tên exported (bắt đầu bằng chữ in hoa) nếu cần truy cập từ package khác.

  6. Hạn chế biến toàn cục (global variables):

    • Biến package-level có thể gây khó kiểm soát trạng thái. Thay vào đó, truyền biến qua tham số hàm.

  7. Kiểm tra lỗi khi gán nhiều giá trị:

    • Khi dùng := với nhiều giá trị, đảm bảo xử lý lỗi đúng cách:

      f, err := os.Open("file.txt")
      if err != nil {
          log.Fatal(err)
      }

7. Các chi tiết nâng cao

7.1. Memory Allocation

  • Biến trong Go được cấp phát trên stack hoặc heap tùy thuộc vào cách sử dụng.

  • Biến cục bộ (local) thường ở stack, nhưng nếu chúng "escape" (ví dụ: được tham chiếu bởi con trỏ hoặc trả về từ hàm), chúng sẽ được cấp phát trên heap.

  • Công cụ go build -gcflags '-m' có thể dùng để phân tích escape analysis:

    func main() {
        x := 42
        p := &x // x escapes to heap
        fmt.Println(*p)
    }

7.2. Type Conversion

  • Go không tự động chuyển đổi kiểu, bạn phải ép kiểu rõ ràng:

    var x int = 42
    var y float64 = float64(x)

7.3. Unused Variables

  • Go không cho phép biến khai báo mà không sử dụng (lỗi biên dịch). Để tạm thời bỏ qua, dùng _:

    _ := 42 // Biến tạm thời không dùng

7.4. Concurrency và Variables

  • Khi làm việc với goroutines, biến cần được quản lý cẩn thận để tránh data race.

  • Dùng sync.Mutex hoặc channel để đồng bộ:

    var counter int
    var mu sync.Mutex
    
    func increment() {
        mu.Lock()
        counter++
        mu.Unlock()
    }

8. Ví dụ thực tế

Dưới đây là một ví dụ kết hợp các khái niệm về biến trong một chương trình thực tế:

package main

import (
    "fmt"
    "sync"
)

var globalCounter int // Package-level variable

const MaxRetries = 3 // Constant

func main() {
    // Short declaration
    name := "Golang Developer"
    retries := 0

    // Explicit declaration
    var wg sync.WaitGroup

    // Function-level variable
    process := func(id int) {
        defer wg.Done()
        for retries < MaxRetries {
            mu.Lock()
            globalCounter++
            fmt.Printf("Worker %d incremented counter to %d\n", id, globalCounter)
            mu.Unlock()
            retries++
        }
    }

    // Launch goroutines
    wg.Add(2)
    go process(1)
    go process(2)

    wg.Wait()
    fmt.Printf("Final counter: %d, Name: %s\n", globalCounter, name)
}

9. Kết luận

Hiểu sâu về biến trong Go không chỉ là nắm cú pháp mà còn là biết cách sử dụng chúng hiệu quả trong các ngữ cảnh thực tế, từ quản lý bộ nhớ, tối ưu hiệu suất, đến xử lý concurrency. Một Senior Golang sẽ luôn cân nhắc scope, type safety, và best practices để viết code rõ ràng, dễ bảo trì, và ít lỗi.

Last updated