目錄

  • 什麼是函數?如何定義?
  • 深入理解函數的參數
  • 參數類型簡寫
  • 可變參數
  • 靈活的函數返回值
  • 多返回值
  • 命名返回值
  • 變量的作用域
  • 局部變量 (Local Variables)
  • 全局變量 (Global Variables)
  • 自定義函數類型
  • 總結

Go語言學習之路-10-go函數_#android

歡迎來到我們的 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 的零值是 0string"")。

在函數體中,你可以直接為這些變量賦值。最後,一個“裸露”的 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

我們可以發現,雖然後續兩次我們把 addsubstract 函數賦值給 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 西三旗