cv.cm

Lập trình · Ngôn ngữ lập trình

Hướng dẫn học lập trình Go (Golang) cơ bản cho người mới

7 phút đọc

Trả lời nhanh

Go (thường gọi là Golang) là ngôn ngữ biên dịch, kiểu tĩnh, đơn giản của Google, được xây dựng để build nhanh, xử lý đồng thời dễ dàng và máy chủ đáng tin cậy. Hãy cài Go, viết một package main và chạy bằng go run hello.go. Các ý tưởng cốt lõi là một tập kiểu nhỏ, slice và map, struct có phương thức, interface ngầm định, goroutine và channel cho đồng thời, và lỗi được trả về như giá trị thông thường.

Mở Định dạng JSON

Go là gì và dùng để làm gì

Go được Robert Griesemer, Rob Pike và Ken Thompson thiết kế tại Google và phát hành mã nguồn mở năm 2009. Nó biên dịch thành một tệp nhị phân tĩnh duy nhất, khởi động nhanh, có bộ thu gom rác và thư viện chuẩn phong phú cho mạng. Go được dùng cho hạ tầng đám mây và công cụ dòng lệnh: Docker, Kubernetes, Terraform và Prometheus đều viết bằng Go, và nhiều công ty dùng nó cho web API và microservice. Ngôn ngữ này được cố ý giữ nhỏ, nên mã của các nhóm khác nhau trông giống nhau.

Cài Go và chạy Hello, world

Tải Go từ go.dev/dl và kiểm tra bằng go version. Tạo một thư mục, khởi tạo module và thêm main.go:

package main

import "fmt"

func main() {
    fmt.Println("Hello, world")
}
go mod init example.com/hello
go run .
# Hello, world
go build     # produces a standalone binary named hello

Việc thực thi bắt đầu tại func main trong package main. Chạy gofmt (hoặc go fmt) để định dạng mã; mọi mã Go dùng cùng một phong cách, với tab để thụt lề trong tệp nguồn thực tế.

Biến và các kiểu dữ liệu cơ bản

Khai báo biến bằng var name type = value, hoặc trong hàm bằng dạng ngắn name := value, tự suy luận kiểu. Biến chưa được gán sẽ nhận giá trị zero: 0, "", false hoặc nil.

KiểuVí dụGiá trị zero
int, int64n := 420
float64x := 3.140
boolok := truefalse
strings := "hi"""
runer := 'é'0 (một code point Unicode)
byteb := byte(65)0 (bí danh của uint8)
package main

import (
    "fmt"
    "strconv"
)

func main() {
    name := "Ada"
    count := 7
    avg := float64(count) / 2 // explicit conversion is required
    fmt.Printf("%s has %d items, avg %.1f\n", name, count, avg) // Ada has 7 items, avg 3.5
    n, err := strconv.Atoi("12")
    fmt.Println(n+1, err) // 13 <nil>
}

Go không bao giờ tự chuyển đổi ngầm giữa các kiểu số, và trình biên dịch từ chối biến không dùng và import không dùng.

Điều khiển luồng và hàm

Điều kiện không có dấu ngoặc đơn, và dấu ngoặc nhọn là bắt buộc. for là từ khóa vòng lặp duy nhất: nó bao gồm vòng lặp cổ điển, kiểu while và range. Hàm có thể trả về nhiều giá trị.

package main

import "fmt"

func divmod(a, b int) (int, int) {
    return a / b, a % b
}

func main() {
    score := 72
    if score >= 90 {
        fmt.Println("A")
    } else if score >= 70 {
        fmt.Println("B") // B
    } else {
        fmt.Println("C")
    }

    for i := 0; i < 3; i++ {
        fmt.Print(i, " ") // 0 1 2
    }
    fmt.Println()

    n := 10
    for n > 1 { // while-style loop
        n /= 2
    }

    q, r := divmod(17, 5)
    fmt.Println(n, q, r) // 1 3 2

    switch day := "SAT"; day {
    case "SAT", "SUN":
        fmt.Println("weekend") // no break needed
    default:
        fmt.Println("weekday")
    }
}

Slice, map và struct

Slice là một khung nhìn co giãn được của mảng và là kiểu danh sách dùng hằng ngày. Map là bảng băm. Struct gom các trường có tên, và phương thức là hàm có receiver.

package main

import (
    "fmt"
    "sort"
)

type Point struct {
    X, Y int
}

func (p Point) Add(o Point) Point { return Point{p.X + o.X, p.Y + o.Y} }

func (p *Point) Scale(k int) { p.X *= k; p.Y *= k } // pointer receiver modifies p

func main() {
    nums := []int{3, 1, 2}
    nums = append(nums, 5)
    sort.Ints(nums)
    fmt.Println(nums, len(nums), nums[1:3]) // [1 2 3 5] 4 [2 3]

    ages := map[string]int{"ann": 31, "bob": 25}
    ages["cy"] = 40
    age, ok := ages["zed"]
    fmt.Println(age, ok, len(ages)) // 0 false 3

    for i, n := range nums {
        fmt.Print(i, ":", n, " ") // 0:1 1:2 2:3 3:5
    }
    fmt.Println()

    p := Point{1, 2}.Add(Point{3, 4})
    p.Scale(2)
    fmt.Printf("%+v\n", p) // {X:8 Y:12}
}

Thứ tự duyệt map được cố ý làm ngẫu nhiên; hãy sắp xếp các khóa nếu cần thứ tự ổn định. Tên bắt đầu bằng chữ hoa (Point, Add) được xuất ra khỏi package.

Interface

Interface liệt kê chữ ký phương thức. Một kiểu thỏa mãn interface chỉ cần có các phương thức đó; không có từ khóa implements.

package main

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
}

type Circle struct{ R float64 }
type Rect struct{ W, H float64 }

func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (r Rect) Area() float64   { return r.W * r.H }

func main() {
    shapes := []Shape{Circle{1}, Rect{2, 3}}
    total := 0.0
    for _, s := range shapes {
        total += s.Area()
    }
    fmt.Printf("%.2f\n", total) // 9.14
}

Go cũng có generic (từ Go 1.18), ví dụ func Max[T int | float64](a, b T) T, mà bạn sẽ dùng chủ yếu qua các hàm thư viện như trong package slices và maps.

Tính năng đặc trưng: goroutine và channel

Goroutine là luồng nhẹ khởi chạy bằng từ khóa go; một chương trình có thể chạy hàng chục nghìn goroutine. Các goroutine giao tiếp qua channel, truyền giá trị an toàn giữa chúng. sync.WaitGroup chờ một nhóm goroutine hoàn thành.

package main

import (
    "fmt"
    "sync"
)

func square(n int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    results <- n * n
}

func main() {
    results := make(chan int, 5)
    var wg sync.WaitGroup
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go square(i, results, &wg)
    }
    wg.Wait()
    close(results)

    sum := 0
    for r := range results {
        sum += r
    }
    fmt.Println(sum) // 55
}

defer hẹn một lời gọi chạy khi hàm bao quanh trả về, đó là cách mã Go đóng tệp và mở khóa mutex. Dùng go run -race . để phát hiện data race khi phát triển.

Xử lý lỗi

Go không có ngoại lệ cho các lỗi thông thường. Hàm có thể thất bại trả về một error làm kết quả cuối, và bên gọi kiểm tra ngay. Bọc lỗi bằng %w để thêm ngữ cảnh mà vẫn kiểm tra được lỗi gốc bằng errors.Is.

package main

import (
    "errors"
    "fmt"
    "os"
)

func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("read config: %w", err)
    }
    return data, nil
}

func main() {
    _, err := readConfig("missing.json")
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("no config file:", err)
    }
}

panic và recover có tồn tại, nhưng dành cho lỗi lập trình và trạng thái thực sự bất ngờ, không dùng để xử lý lỗi thông thường.

Một chương trình nhỏ hoàn chỉnh: máy chủ web nhỏ

Chỉ với thư viện chuẩn, chương trình này phục vụ JSON qua HTTP. Chạy bằng go run . và mở http://localhost:8080/hello?name=Ann.

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type Greeting struct {
    Message string    `json:"message"`
    Time    time.Time `json:"time"`
}

func hello(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
        name = "world"
    }
    w.Header().Set("Content-Type", "application/json")
    err := json.NewEncoder(w).Encode(Greeting{"Hello, " + name, time.Now()})
    if err != nil {
        log.Println("encode:", err)
    }
}

func main() {
    http.HandleFunc("/hello", hello)
    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Đoạn văn bản sau kiểu của mỗi trường là struct tag đặt tên khóa JSON. Mỗi yêu cầu được xử lý trong goroutine riêng một cách tự động.

Những lỗi thường gặp của người mới

  • Bỏ qua lỗi bằng _. Hãy kiểm tra mọi error được trả về trừ khi bạn chắc chắn nó không quan trọng.
  • Mong append thay đổi slice gốc tại chỗ; hãy luôn dùng kết quả trả về, như s = append(s, x).
  • Khởi chạy goroutine rồi để main kết thúc trước khi chúng xong. Hãy chờ bằng sync.WaitGroup hoặc channel.
  • Dùng value receiver khi phương thức phải sửa struct; hãy dùng pointer receiver như func (p *Point).

Học tiếp ở đâu

Hãy làm Tour of Go tương tác, rồi đọc Effective Go và tài liệu thư viện chuẩn tại pkg.go.dev. Học go test cho kiểm thử đơn vị và benchmark, package context cho timeout và hủy bỏ, và module cho các phụ thuộc.

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

Gọi là Go hay Golang?

Tên chính thức là Go. "Golang" xuất phát từ địa chỉ website cũ golang.org và vẫn được dùng làm từ khóa tìm kiếm.

Go có hướng đối tượng không?

Một phần. Go có phương thức và interface nhưng không có lớp hay kế thừa; bạn xây dựng kiểu bằng cách nhúng struct và kết hợp các interface nhỏ.

Go có nhanh hơn Python không?

Với hầu hết công việc nặng về CPU thì có, vì Go biên dịch ra mã máy gốc và có kiểu tĩnh. Python vẫn có thể nhanh khi phần việc nặng nằm trong các thư viện viết bằng C.

Goroutine và luồng (thread) khác nhau thế nào?

Goroutine được runtime của Go quản lý và bắt đầu với stack nhỏ tăng dần khi cần. Runtime lập lịch nhiều goroutine lên một vài luồng của hệ điều hành, nên tạo goroutine rẻ hơn nhiều.

Tất cả hướng dẫn lập trình