目錄
- 什麼是函數?如何定義?
- 深入理解函數的參數
- 參數類型簡寫
- 可變參數
- 靈活的函數返回值
- 多返回值
- 命名返回值
- 變量的作用域
- 局部變量 (Local Variables)
- 全局變量 (Global Variables)
- 自定義函數類型
- 總結
歡迎來到我們的 Go 語言學習系列!如果説變量是編程語言中的“名詞n.”,那麼函數就是“動詞v.”。它們是執行特定任務、組織代碼和實現複用的核心單元。
在 Go 語言中,函數的設計簡潔而強大。本篇文章是函數系列的第一篇,我們將深入探討函數的基礎知識:如何定義、如何傳遞參數、如何獲取返回值,以及它們的作用域規則。
在下一篇博文中,我們才會“解鎖”更高階的用法,如匿名函數、閉包、以及將函數作為參數和返回值。現在,讓我們從最基礎的開始。
什麼是函數?如何定義?
在 Go 中,函數是組織好的、可重複使用的、用於執行指定任務的代碼塊。我們使用 func 關鍵字來定義一個函數。
其基本語法結構如下:
func 函數名(參數列表) (返回值列表) {
// 函數體
// ...
return 返回值
}
func:定義函數的關鍵字。函數名:函數的唯一標識符,固在其作用域區間內不能重名。參數列表:包含數個形參,負責函數接收的輸入。每個參數都有一個名字和一個類型。返回值列表:函數執行後返回的輸出。同樣,每個返回值都有一個類型(也可以有名字)。函數體:花括號{}之間的代碼,是函數的真正“工作區”。
讓我們來看一個最簡單的例子:計算兩個整數的和。
package main
import "fmt"
// 定義一個函數 sumFn,它接收兩個 int 類型的參數 x 和 y
// 並返回一個 int 類型的結果
func sumFn(x int, y int) int {
sum := x + y
return sum
}
func main() {
// 調用函數,並傳入參數 12 和 3
result := sumFn(12, 3)
// 打印結果
fmt.Println("12 + 3 =", result) // 輸出: 12 + 3 = 15
}
在這個例子中,sumFn 是我們的函數名,(x int, y int) 是參數列表,int 是返回值列表(因為只有一個返回值),return sum 負責將計算結果返回給調用者。
深入理解函數的參數
參數是函數與外界交換數據的“入口”。Go 語言在參數設計上提供了幾個便捷的特性。
參數類型簡寫
當多個連續的參數具有相同類型時,你不需要為每一個都寫上類型,只需在最後一個參數後聲明類型即可。這能讓你的函數簽名更簡潔:
// 原始寫法
func add(x int, y int) int {
return x + y
}
// 簡寫
func multiply(x, y int) int {
return x * y
}
func main() {
fmt.Println(add(10, 5)) // 輸出: 15
fmt.Println(multiply(10, 5)) // 輸出: 50
}
func multiply(x, y int) 和 func multiply(x int, y int) 是完全等價的。
可變參數
有時候,你可能不確定一個函數需要接收多少個參數。例如,你可能想寫一個函數來計算任意多個數字的和。
這時,"可變參數"就派上用場了。通過在參數名後加上 ... 來標識,它必須是函數簽名的最後一個參數。
package main
import "fmt"
// (numbers ...int) 表示 numbers 是一個可變參數
// 它接收 0 個或多個 int 類型的參數
func sumAll(numbers ...int) int {
// 在函數內部, 'numbers' 的類型是一個 int 類型的切片 (slice): []int
fmt.Printf("接收到的參數: %v, 它的類型是: %T\n", numbers, numbers)
total := 0
for _, num := range numbers {
total += num
}
return total
}
func main() {
// 我們可以傳遞任意多個參數
sum1 := sumAll(1, 2)
fmt.Println("Sum 1:", sum1) // 輸出: Sum 1: 3
sum2 := sumAll(1, 2, 3, 4, 5)
fmt.Println("Sum 2:", sum2) // 輸出: Sum 2: 15
// 甚至可以不傳遞參數
sum3 := sumAll()
fmt.Println("Sum 3:", sum3) // 輸出: Sum 3: 0
}
當你調用 sumAll(1, 2, 3, 4, 5) 時,Go 語言在底層會創建一個切片 []int{1, 2, 3, 4, 5} 並將其傳遞給 numbers 變量作為形參。
靈活的函數返回值
Go 語言的返回值設計是其一大亮點,它原生支持返回多個值。
多返回值
這在其他語言中(如 C++ 或 Java)通常需要返回一個對象或結構體才能實現。在 Go 中,這非常簡單,請見如下示例:
package main
import "fmt"
// 這個函數返回兩個 int 類型的值
func calc(x, y int) (int, int) {
sum := x + y
sub := x - y
return sum, sub
}
func main() {
// 我們可以用兩個變量來接收這兩個返回值
a, b := calc(10, 2)
fmt.Printf("10+2 = %d\n", a) // 輸出: 10+2 = 12
fmt.Printf("10-2 = %d\n", b) // 輸出: 10-2 = 8
}
命名返回值
Go 還允許你為返回值命名。這些命名的返回值在函數開始時就被隱式聲明為變量(注意是形參),並初始化為它們的零值(例如 int 的零值是 0,string 是 "")。
在函數體中,你可以直接為這些變量賦值。最後,一個“裸露”的 return 語句(即不帶任何參數的 return)會自動返回這些命名變量的當前值。
package main
import "fmt"
// 返回值被命名為 sum 和 sub
func calcNamed(x, y int) (sum int, sub int) {
// 此時, sum 和 sub 已經被聲明,並且其零值都為 0
fmt.Printf("初始值: sum=%d, sub=%d\n", sum, sub)
sum = x + y // 直接給命名返回值賦值
sub = x - y // 直接給命名返回值賦值
// "裸露" return 會自動返回 sum 和 sub 的當前值
return
}
func main() {
s, d := calcNamed(100, 50)
// 輸出:
// 初始值: sum=0, sub=0
// 100+50 = 150
// 100-50 = 50
fmt.Printf("100+50 = %d\n", s)
fmt.Printf("100-50 = %d\n", d)
}
注意: 雖然“裸露”return 很方便,但在較長的函數中,它可能會降低代碼的可讀性。我們推薦在簡短、清晰的函數中使用它。
變量的作用域
作用域(Scope)決定了變量的可見性和生命週期。在 Go 中,主要分為兩類:
局部變量 (Local Variables)
- 在函數內部聲明的變量(包括函數的參數)。
- 它們只在該函數的花括號
{}內部可見和可用。 - 當函數執行完畢返回時,這些局部變量就會被銷燬。
全局變量 (Global Variables)
- 在所有函數(包括
main函數)之外聲明的變量。 - 它們在整個包(
package)的所有文件中都可見和可用。 - 它們的生命週期貫穿程序的整個運行時間。
package main
import "fmt"
// 這是一個全局變量 globalMsg
var globalMsg = "Hello, I am global"
func printLocal() {
// 這是一個局部變量 localMsg
var localMsg = "Hi, I am local"
fmt.Println(localMsg) // OK: 可以訪問局部變量
fmt.Println(globalMsg) // OK: 也可以訪問全局變量
}
func main() {
printLocal()
fmt.Println(globalMsg) // OK: main 函數也可以訪問全局變量
// fmt.Println(localMsg) // 編譯錯誤! 無法訪問 printLocal 函數的局部變量
}
最佳實踐: 應儘量減少全局變量的使用。全局變量會增加代碼的複雜性和耦合度,使其難以維護和測試。優先通過函數參數和返回值來傳遞數據。
自定義函數類型
最後,Go 語言允許我們使用 type 關鍵字來定義自己的函數類型。
這聽起來有點抽象,但它其實就是給一個特定的“函數簽名”(即參數和返回值的組合)起一個別名。
// 我們定義了一個新的類型 'calculation'
// 它代表了“任何接收兩個 int 並返回一個 int”的函數
type calculation func(int, int) int
一旦定義了這個類型,我們就可以聲明該類型的變量,並將任何符合該簽名的函數賦值給它。
package main
import "fmt"
// 1. 定義一個函數類型
type calculation func(int, int) int
// 2. 編寫幾個符合這個簽名的函數
func add(x, y int) int {
return x + y
}
func subtract(x, y int) int {
return x - y
}
func main() {
// 3. 聲明一個 'calculation' 類型的變量
var op calculation
// 4. 我們可以將 add 函數賦值給 op
op = add
result := op(10, 5) // 就像調用 add(10, 5) 一樣
fmt.Println("Add Result:", result) // 輸出: Add Result: 15
// 5. 我們也可以將 subtract 函數賦值給 op
op = subtract
result = op(10, 5) // 就像調用 subtract(10, 5) 一樣
fmt.Println("Subtract Result:", result) // 輸出: Subtract Result: 5
}
我們來探究一下自定義函數 op 在前後三次聲明和賦值時其類型有何變化。在 main 主函數體內部補充為如下內容:
// 3. 聲明一個 'calculation' 類型的變量
var op calculation
fmt.Printf("Type of OP: %T \n", op)
// 4. 我們可以將 add 函數賦值給 op
op = add
result := op(10, 5) // 就像調用 add(10, 5) 一樣
fmt.Println("Add Result:", result) // 輸出: Add Result: 15
fmt.Printf("Type of OP: %T \n", op)
// 5. 我們也可以將 subtract 函數賦值給 op
op = subtract
result = op(10, 5) // 就像調用 subtract(10, 5) 一樣
fmt.Println("Subtract Result:", result) // 輸出: Subtract Result: 5
fmt.Printf("Type of OP: %T \n", op)
結果輸出如下:
Type of OP: main.calculation
Add Result: 15
Type of OP: main.calculation
Subtract Result: 5
Type of OP: main.calculation
我們可以發現,雖然後續兩次我們把 add 和 substract 函數賦值給 op,但是其類型一直保持為我們自定義的函數類型 calculation。
那麼,對於自定義的函數類型,有什麼要求才能使得能被兩個函數 add 以及 substract 賦值呢?
最基本的一個要求就是參數值與返回值類型和數量能夠對得上。如果我們自定義的函數類型 calculation 為:
// 參數多一個
type calculation func(int, int, int) int
// 或參數類型不同
type calculation func(int, string) int
// 或返回值多一個
type calculation func(int, int) (int, int)
都會導致無法將 add 函數以及 substract 函數成功賦值給 op。
自定義函數類型是實現 Go 語言高階函數(如將函數作為參數傳遞)的基石,我們將在下一篇文章中詳細探討它的威力。
總結
恭喜你!你已經完成了 Go 語言函數基礎知識的學習。我們回顧一下:
- 函數定義: 使用
func關鍵字,包含函數名、參數、返回值和函數體。 - 函數參數: 支持類型簡寫
(x, y int)和強大的可變參數(nums ...int)。 - 函數返回: Go 語言的特色——支持多返回值和命名返回值。
- 作用域: 瞭解了局部變量(函數內)和全局變量(函數外)的區別。
- 函數類型: 使用
type關鍵字可以為函數簽名創建一個新類型。
掌握這些基礎是至關重要的。它們是你構建更復雜、更優雅的 Go 程序的“積木”。
在下一篇博文中,我們將踏入更激動人心的領域:匿名函數、閉包,以及如何將函數在你的代碼中傳來傳去。敬請期待!
2025.10.22 西三旗