本文 AI 產出,尚未審核

Golang – 函數與方法

主題:函數定義與呼叫


簡介

在任何程式語言中,**函數(function)**都是最基本的抽象單位。它讓我們把重複的程式碼封裝起來,提升可讀性、可維護性,同時也為測試與重構奠定基礎。Go 語言設計上特別強調 簡潔高效,因此函數的語法與使用方式相當直觀,但仍有許多細節值得新手留意。

本篇文章將從 函數的宣告、參數與回傳值呼叫方式,到 方法(method) 的差異與實作,逐層剖析。每個概念都配上實務範例,讓讀者在閱讀完後,能立即在自己的專案中套用,從基礎寫出乾淨、可測試的 Go 程式碼。


核心概念

1. 基本函數宣告

Go 的函數使用 func 關鍵字,語法如下:

func 函數名稱(參數列表) (回傳值列表) {
    // 函式本體
}
  • 參數列表:每個參數必須寫出名稱與型別,若多個參數型別相同,可合併寫在最後,例如 a, b int
  • 回傳值列表:可以是單一值、具名回傳值或多個回傳值。

範例 1:最簡單的「Hello, World」函數

package main

import "fmt"

// 無參數、無回傳值的函式
func hello() {
    fmt.Println("Hello, World!")
}

func main() {
    hello() // 呼叫函式
}

這個例子展示了最基礎的函數結構:沒有參數、沒有回傳值,只負責輸出訊息。

2. 帶參數與回傳值

範例 2:計算兩數相加

package main

import "fmt"

// a 與 b 為 int 型別,回傳值也是 int
func add(a int, b int) int {
    return a + b
}

func main() {
    sum := add(3, 5)
    fmt.Printf("3 + 5 = %d\n", sum)
}
  • 參數 ab 必須在呼叫時提供相同型別的實際值。
  • return 後面的表達式即為回傳值。

範例 3:多回傳值 – 同時回傳結果與錯誤

package main

import (
    "errors"
    "fmt"
)

// 除法,若除數為 0 則回傳錯誤
func divide(dividend, divisor float64) (float64, error) {
    if divisor == 0 {
        return 0, errors.New("除數不能為 0")
    }
    return dividend / divisor, nil
}

func main() {
    if result, err := divide(10, 2); err == nil {
        fmt.Printf("10 / 2 = %.2f\n", result)
    }

    // 錯誤情況
    if _, err := divide(10, 0); err != nil {
        fmt.Println("錯誤:", err)
    }
}
  • 多回傳值 是 Go 的一大特色,讓錯誤處理變得自然。
  • 使用 if result, err := …; err == nil { … } 可在同一行完成宣告與判斷,提升程式碼可讀性。

3. 具名回傳值與 defer

具名回傳值會在函式頭部先宣告變數名稱與型別,函式內部可直接使用,最後 return 時可省略回傳值。

package main

import "fmt"

// 具名回傳值
func swap(a, b string) (first string, second string) {
    first = b   // 直接對具名變數賦值
    second = a
    return      // 等同於 return first, second
}

func main() {
    x, y := swap("hello", "world")
    fmt.Println(x, y) // output: world hello
}

defer 常與具名回傳值結合,用於在函式結束前執行清理或統計工作:

package main

import "fmt"

func compute() (result int) {
    defer func() {
        fmt.Println("計算結束,結果為", result)
    }()
    result = 42
    return // 仍會觸發 defer
}

func main() {
    compute()
}

4. 可變參數(Variadic)

若函式需要接受不定數量的同型別參數,可使用 ... 語法:

package main

import "fmt"

// 計算任意多個整數的總和
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(sum(1, 2, 3, 4)) // 10
    slice := []int{5, 6, 7}
    fmt.Println(sum(slice...))   // 18,使用 slice 展開
}
  • 注意:可變參數必須是最後一個參數,且在呼叫時可以直接傳入切片(使用 slice...)。

5. 方法(Method)與接收者

在 Go 中,方法是「綁定在型別(通常是 struct)上的函式**。** 方法的第一個參數稱為 接收者(receiver),它決定了方法屬於哪個型別。

package main

import "fmt"

// 定義一個簡單的結構體
type Rectangle struct {
    Width, Height float64
}

// 方法:計算面積(使用值接收者)
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// 方法:設定寬度(使用指標接收者,才能修改原始值)
func (r *Rectangle) SetWidth(w float64) {
    r.Width = w
}

func main() {
    rect := Rectangle{Width: 3, Height: 4}
    fmt.Println("面積:", rect.Area()) // 12

    rect.SetWidth(5)
    fmt.Println("新寬度:", rect.Width) // 5
}
  • 值接收者 (r Rectangle) 會在呼叫時複製整個結構體,適合不需要修改原始資料的情況。
  • 指標接收者 (r *Rectangle) 允許在方法內部直接修改結構體的欄位,且避免不必要的複製。

常見陷阱與最佳實踐

陷阱 說明 建議的做法
忘記 return 多回傳值函式若缺少 return 會編譯錯誤。 確認每條執行路徑都有 return,或使用具名回傳值搭配 return 省略。
可變參數只能放最後 func foo(a int, b ...int) 正確,func foo(...int, a int) 錯誤。 依照語法規則將 ... 放在最後一個參數。
指標接收者與值接收者混用 同一型別的兩個方法分別使用指標與值接收者,可能導致不一致的行為。 若型別需要修改,統一使用指標接收者;若僅讀取,使用值接收者。
忘記切片展開 呼叫可變參數時直接傳入 slice 會編譯錯誤。 使用 slice... 進行展開。
錯誤忽略 多回傳值的 error 常被忽略,導致隱蔽 bug。 永遠檢查 error,即使暫時不處理,也要記錄或回傳。

最佳實踐

  1. 單一職責:每個函式只做一件事,保持簡潔。
  2. 明確命名:函式名稱應描述其行為,例如 ParseJSONValidateUser
  3. 錯誤即時處理:使用 if err != nil { … } 立即回應錯誤,避免錯誤傳遞過深。
  4. 使用 defer 釋放資源:如檔案、網路連線,確保在函式結束時釋放。
  5. 避免全域變數:將需要的資料透過參數傳入,提升可測試性。

實際應用場景

1. API 伺服器的請求處理

net/http 套件中,處理器函式 必須符合 func(w http.ResponseWriter, r *http.Request) 簽名。開發者常把業務邏輯抽成獨立函式,讓測試更容易。

package main

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

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

// 真正的業務邏輯
func getUserByID(id int) (User, error) {
    // 假設從資料庫查詢
    if id == 0 {
        return User{}, errors.New("invalid ID")
    }
    return User{ID: id, Name: "Alice"}, nil
}

// HTTP 處理器
func userHandler(w http.ResponseWriter, r *http.Request) {
    // 解析 query 參數
    ids, ok := r.URL.Query()["id"]
    if !ok || len(ids[0]) < 1 {
        http.Error(w, "missing id", http.StatusBadRequest)
        return
    }

    // 呼叫業務函式
    user, err := getUserByID(strings.Atoi(ids[0]))
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // 回傳 JSON
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}
  • 分層userHandler 只負責 HTTP 相關的事務,真正的資料取得交給 getUserByID
  • 錯誤傳遞getUserByID 回傳 error,讓 handler 可以根據情況回傳不同的 HTTP 狀態碼。

2. 資料處理的管線(Pipeline)

利用 可變參數函式作為第一級公民,可以快速組合資料處理流程。

package main

import "fmt"

// Filter 函式類型:接受 int,回傳 bool
type Filter func(int) bool

// pipeline 接收一組資料與多個 Filter,返回符合所有條件的結果
func pipeline(data []int, filters ...Filter) []int {
    var result []int
    for _, v := range data {
        ok := true
        for _, f := range filters {
            if !f(v) {
                ok = false
                break
            }
        }
        if ok {
            result = append(result, v)
        }
    }
    return result
}

// 範例過濾條件
func isEven(n int) bool { return n%2 == 0 }
func lessThanTen(n int) bool { return n < 10 }

func main() {
    nums := []int{1, 2, 3, 4, 10, 12, 14}
    filtered := pipeline(nums, isEven, lessThanTen)
    fmt.Println(filtered) // [2 4]
}
  • 可變參數pipeline 能接受任意數量的過濾條件,極大提升彈性。
  • 這種寫法在 資料流處理、ETL、測試生成 等場景非常常見。

總結

  • 函數是 Go 程式設計的核心抽象,掌握參數、回傳值、具名回傳與 defer,能寫出簡潔且安全的程式。
  • 可變參數多回傳值 為錯誤處理與彈性設計提供了便利。
  • 方法透過接收者將行為綁定到型別,讓資料與行為保持一致,使用指標接收者可避免不必要的複製並支援狀態變更。
  • 常見陷阱如忘記 return、錯誤忽略、接收者混用等,只要遵循最佳實踐(單一職責、明確命名、即時錯誤處理),即可大幅提升程式品質。
  • API 伺服器資料管線 等實務情境中,將業務邏輯抽成純函式,配合方法與介面,能讓程式更易測試、易維護。

透過本文的概念與範例,你已具備在 Go 專案中 定義與呼叫函數 的能力,接下來可以進一步探索 介面(interface)併發(goroutine)錯誤封裝 等更進階的主題。祝你在 Go 的世界裡寫出更好、更安全的程式碼!