這是 Go 語言從零到 Web 應用系列的第四篇。上一篇學了基礎語法,這篇要來認識 Go 的核心資料結構。

Array(陣列)

Array 是固定長度的同型別元素集合。

// 宣告陣列(長度是型別的一部分)
var scores [5]int  // [0, 0, 0, 0, 0]

// 初始化
scores = [5]int{90, 85, 78, 92, 88}

// 簡短宣告
names := [3]string{"Alice", "Bob", "Charlie"}

// 讓編譯器計算長度
primes := [...]int{2, 3, 5, 7, 11}

// 存取元素
fmt.Println(scores[0])  // 90
scores[1] = 95

// 取得長度
fmt.Println(len(scores))  // 5

Array 在 Go 中較少直接使用,因為長度固定且是值語意(傳遞時會複製整個陣列)。大多數情況我們用 Slice。

Slice(切片)

Slice 是 Go 中最常用的集合型別,可以視為動態長度的陣列。

建立 Slice

// 從 array 建立
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4]  // [2, 3, 4](左包含,右不包含)

// 直接宣告
nums := []int{10, 20, 30}

// 使用 make(指定長度和容量)
s1 := make([]int, 5)     // 長度 5,容量 5
s2 := make([]int, 3, 10) // 長度 3,容量 10

長度 vs 容量

s := make([]int, 3, 5)
fmt.Println(len(s))  // 3(目前元素數量)
fmt.Println(cap(s))  // 5(底層陣列的大小)

append:新增元素

nums := []int{1, 2, 3}
nums = append(nums, 4)          // [1, 2, 3, 4]
nums = append(nums, 5, 6, 7)    // [1, 2, 3, 4, 5, 6, 7]

// 合併兩個 slice
more := []int{8, 9}
nums = append(nums, more...)     // [1, 2, 3, 4, 5, 6, 7, 8, 9]

當 append 超過容量時,Go 會自動分配更大的底層陣列。

Slice 操作

s := []int{0, 1, 2, 3, 4, 5}

// 切片語法
s[2:4]  // [2, 3]
s[:3]   // [0, 1, 2]
s[3:]   // [3, 4, 5]
s[:]    // [0, 1, 2, 3, 4, 5](複製參考)

// 刪除元素(刪除 index 2)
s = append(s[:2], s[3:]...)  // [0, 1, 3, 4, 5]

// 複製 slice
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)

注意:Slice 是參考型別

original := []int{1, 2, 3}
alias := original      // alias 和 original 指向同一底層陣列

alias[0] = 99
fmt.Println(original)  // [99, 2, 3]  ← 被改了!

// 要獨立複本,用 copy
independent := make([]int, len(original))
copy(independent, original)

Map(映射)

Map 是鍵值對的集合,類似其他語言的 Dictionary 或 HashMap。

建立 Map

// 使用 make
ages := make(map[string]int)
ages["Alice"] = 30
ages["Bob"] = 25

// 直接初始化
scores := map[string]int{
    "Alice": 95,
    "Bob":   87,
    "Carol": 92,
}

操作 Map

m := map[string]int{
    "apple":  5,
    "banana": 3,
}

// 讀取
count := m["apple"]  // 5

// 檢查鍵是否存在(重要!)
value, exists := m["orange"]
if exists {
    fmt.Println("Found:", value)
} else {
    fmt.Println("Not found")
}

// 新增或修改
m["cherry"] = 8

// 刪除
delete(m, "banana")

// 取得長度
fmt.Println(len(m))

// 遍歷(順序不保證)
for key, value := range m {
    fmt.Printf("%s: %d\n", key, value)
}

Map 的零值是 nil

var m map[string]int  // m 是 nil
// m["key"] = 1       // panic! 不能寫入 nil map

// 必須先初始化
m = make(map[string]int)
m["key"] = 1  // OK

Struct(結構體)

Struct 是 Go 定義自訂型別的主要方式,類似其他語言的 class(但沒有繼承)。

定義和使用

type User struct {
    Name  string
    Email string
    Age   int
}

// 建立實例
u1 := User{
    Name:  "Jack",
    Email: "[email protected]",
    Age:   30,
}

// 也可以省略欄位名(按順序)
u2 := User{"Alice", "[email protected]", 25}

// 存取欄位
fmt.Println(u1.Name)  // Jack
u1.Age = 31

// 零值 struct
var u3 User  // Name: "", Email: "", Age: 0

巢狀 Struct

type Address struct {
    City    string
    Country string
}

type Person struct {
    Name    string
    Age     int
    Address Address  // 巢狀 struct
}

p := Person{
    Name: "Jack",
    Age:  30,
    Address: Address{
        City:    "Taipei",
        Country: "Taiwan",
    },
}

fmt.Println(p.Address.City)  // Taipei

匿名欄位(嵌入)

type Person struct {
    Name string
    Age  int
    Address  // 匿名嵌入,直接用型別名稱
}

p := Person{
    Name: "Jack",
    Age:  30,
    Address: Address{
        City:    "Taipei",
        Country: "Taiwan",
    },
}

// 可以直接存取嵌入 struct 的欄位
fmt.Println(p.City)  // Taipei(不需要 p.Address.City)

Struct 指標

u := &User{Name: "Jack", Email: "[email protected]", Age: 30}

// Go 會自動解參考,不需要寫 (*u).Name
fmt.Println(u.Name)  // Jack

Struct 標籤(Tags)

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age,omitempty"`
}

標籤是附加的中繼資料,最常見的用途是 JSON 序列化。omitempty 表示零值時省略該欄位。我們在做 Web API 時會大量使用。

Method(方法)

Method 是附加到型別上的函式。

值接收者

type Rectangle struct {
    Width  float64
    Height float64
}

// Area 是 Rectangle 的方法
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// 使用
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area())       // 50
fmt.Println(rect.Perimeter())  // 30

(r Rectangle) 稱為接收者(receiver),等同於其他語言的 thisself

指標接收者

如果方法需要修改接收者的狀態,使用指標接收者:

func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

rect := Rectangle{Width: 10, Height: 5}
rect.Scale(2)
fmt.Println(rect.Width)   // 20
fmt.Println(rect.Height)  // 10

什麼時候用指標接收者?

  • 需要修改接收者的狀態
  • 接收者是大型 struct(避免複製的開銷)
  • 如果型別的任何一個方法使用指標接收者,那所有方法都應該使用指標接收者(一致性)

方法的語法糖

rect := Rectangle{Width: 10, Height: 5}

// 以下兩種寫法等價
rect.Scale(2)

// 實際上 Go 自動做了 (&rect).Scale(2)

Go 會根據方法的接收者型別自動取地址或解參考。

Interface(介面)

Interface 定義了一組方法簽名。任何型別只要實作了這些方法,就自動滿足該 interface——不需要明確宣告。

定義 Interface

type Shape interface {
    Area() float64
    Perimeter() float64
}

隱式實作

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Circle 自動滿足 Shape interface
// 不需要寫 "implements Shape" 之類的宣告

使用 Interface

func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
    fmt.Printf("Perimeter: %.2f\n", s.Perimeter())
}

rect := Rectangle{Width: 10, Height: 5}
circle := Circle{Radius: 7}

printShapeInfo(rect)    // 都可以傳入
printShapeInfo(circle)  // 因為都滿足 Shape interface

空 Interface

interface{} 或 Go 1.18+ 的 any 可以接受任何型別的值:

func printAnything(v any) {
    fmt.Printf("Type: %T, Value: %v\n", v, v)
}

printAnything(42)
printAnything("hello")
printAnything(true)

型別斷言

var i any = "hello"

// 型別斷言
s := i.(string)
fmt.Println(s)  // hello

// 安全的型別斷言
s, ok := i.(string)
if ok {
    fmt.Println("It's a string:", s)
}

// type switch
switch v := i.(type) {
case string:
    fmt.Println("String:", v)
case int:
    fmt.Println("Int:", v)
default:
    fmt.Println("Unknown type")
}

常用的標準庫 Interface

// fmt.Stringer — 自訂印出格式
type Stringer interface {
    String() string
}

type User struct {
    Name string
    Age  int
}

func (u User) String() string {
    return fmt.Sprintf("%s (%d)", u.Name, u.Age)
}

u := User{Name: "Jack", Age: 30}
fmt.Println(u)  // Jack (30)

// error — Go 的錯誤介面
type error interface {
    Error() string
}

組合(Composition)vs 繼承

Go 沒有繼承,取而代之的是組合。

其他語言的繼承

# Python 的繼承
class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

Go 的組合

type Animal struct {
    Name string
}

func (a Animal) Describe() string {
    return fmt.Sprintf("I am %s", a.Name)
}

type Dog struct {
    Animal     // 嵌入 Animal
    Breed string
}

func (d Dog) Speak() string {
    return "Woof!"
}

dog := Dog{
    Animal: Animal{Name: "Buddy"},
    Breed:  "Labrador",
}

fmt.Println(dog.Describe())  // I am Buddy(提升的方法)
fmt.Println(dog.Speak())     // Woof!
fmt.Println(dog.Name)        // Buddy(提升的欄位)

組合的優勢:

  • 更靈活:可以嵌入多個型別
  • 更明確:沒有複雜的繼承層級
  • 更容易理解:看型別定義就知道它有什麼能力

練習:建立一個簡單的待辦清單

package main

import "fmt"

type Todo struct {
    ID    int
    Title string
    Done  bool
}

func (t Todo) String() string {
    status := "[ ]"
    if t.Done {
        status = "[x]"
    }
    return fmt.Sprintf("%s %d. %s", status, t.ID, t.Title)
}

type TodoList struct {
    todos  []Todo
    nextID int
}

func NewTodoList() *TodoList {
    return &TodoList{nextID: 1}
}

func (tl *TodoList) Add(title string) {
    tl.todos = append(tl.todos, Todo{
        ID:    tl.nextID,
        Title: title,
        Done:  false,
    })
    tl.nextID++
}

func (tl *TodoList) Complete(id int) bool {
    for i := range tl.todos {
        if tl.todos[i].ID == id {
            tl.todos[i].Done = true
            return true
        }
    }
    return false
}

func (tl *TodoList) List() {
    for _, t := range tl.todos {
        fmt.Println(t)
    }
}

func main() {
    list := NewTodoList()
    list.Add("學習 Go 基礎語法")
    list.Add("學習 Go 資料結構")
    list.Add("建立第一個 Web Server")

    list.Complete(1)

    fmt.Println("=== 待辦清單 ===")
    list.List()
}

輸出:

=== 待辦清單 ===
[x] 1. 學習 Go 基礎語法
[ ] 2. 學習 Go 資料結構
[ ] 3. 建立第一個 Web Server

下一步

學完了 Go 的核心資料結構,下一篇我們將進入 Go 最有特色的兩個主題:錯誤處理併發。這兩個主題是 Go 區別於其他語言的核心設計,也是寫好 Go 程式的關鍵。