本文 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)
}
- 參數
a、b必須在呼叫時提供相同型別的實際值。 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,即使暫時不處理,也要記錄或回傳。 |
最佳實踐
- 單一職責:每個函式只做一件事,保持簡潔。
- 明確命名:函式名稱應描述其行為,例如
ParseJSON、ValidateUser。 - 錯誤即時處理:使用
if err != nil { … }立即回應錯誤,避免錯誤傳遞過深。 - 使用
defer釋放資源:如檔案、網路連線,確保在函式結束時釋放。 - 避免全域變數:將需要的資料透過參數傳入,提升可測試性。
實際應用場景
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 的世界裡寫出更好、更安全的程式碼!