鍍金池/ 教程/ GO/ 6.2 函數(shù)參數(shù)與返回值
4.7 strings 和 strconv 包
13.6 啟動外部命令和程序
?# 11.4 類型判斷:type-switch
12.1 讀取用戶的輸入
10.6 方法
12.2 文件讀寫
13 錯誤處理與測試
9.3 鎖和 sync 包
12.3 文件拷貝
?# 11.7 第一個例子:使用 Sorter 接口排序
?# 11.5 測試一個值是否實現(xiàn)了某個接口
6.4 defer 和追蹤
12.10 XML 數(shù)據格式
13.10 性能調試:分析并優(yōu)化 Go 程序
?# 11.1 接口是什么
2.2 Go 環(huán)境變量
2.6 安裝目錄清單
2.5 在 Windows 上安裝 Go
11.11 Printf 和反射
1.2 語言的主要特性與發(fā)展的環(huán)境和影響因素
9.0 包(package)
7.4 切片重組(reslice)
13.2 運行時異常和 panic
10.2 使用工廠方法創(chuàng)建結構體實例
12.8 使用接口的實際例子:fmt.Fprintf
2.4 在 Mac OS X 上安裝 Go
3.8 Go 性能說明
7.2 切片
8.0 Map
3.1 Go 開發(fā)環(huán)境的基本要求
5.6 標簽與 goto
6.10 使用閉包調試
9.5 自定義包和可見性
4.3 常量
?# 11.2 接口嵌套接口
6.5 內置函數(shù)
前言
10.8 垃圾回收和 SetFinalizer
2.8 Go 解釋器
13.7 Go 中的單元測試和基準測試
6.8 閉包
4.9 指針
13.1 錯誤處理
10.1 結構體定義
5.1 if-else 結構
6.6 遞歸函數(shù)
9.9 通過 Git 打包和安裝
2.7 Go 運行時(runtime)
10.7 類型的 String() 方法和格式化描述符
3.7 其它工具
9.6 為自定義包使用 godoc
11.12 接口與動態(tài)類型
13.3 從 panic 中恢復(Recover)
10.3 使用自定義包中的結構體
11.14 結構體、集合和高階函數(shù)
3.6 生成代碼文檔
9.2 regexp 包
4.1 文件名、關鍵字與標識符
?# 11.6 使用方法集與接口
7.0 數(shù)組與切片
7.1 聲明和初始化
12.11 用 Gob 傳輸數(shù)據
5.5 Break 與 continue
1.1 起源與發(fā)展
?# 11 接口(Interfaces)與反射(reflection)
6.9 應用閉包:將函數(shù)作為返回值
4.2 Go 程序的基本結構和要素
8.6 將 map 的鍵值對調
6.11 計算函數(shù)執(zhí)行時間
5.0 控制結構
10.5 匿名字段和內嵌結構體
4.6 字符串
3.0 編輯器、集成開發(fā)環(huán)境與其它工具
13.8 測試的具體例子
7.6 字符串、數(shù)組和切片的應用
8.4 map 類型的切片
3.9 與其它語言進行交互
7.3 For-range 結構
9.7 使用 go install 安裝自定義包
6.0 函數(shù)
9.8 自定義包的目錄結構、go install 和 go test
6.3 傳遞變長參數(shù)
13.9 用(測試數(shù)據)表驅動測試
11.9 空接口
8.1 聲明、初始化和 make
6.2 函數(shù)參數(shù)與返回值
9.11 在 Go 程序中使用外部庫
3.3 調試器
4.5 基本類型和運算符
?# 11.8 第二個例子:讀和寫
12.5 用 buffer 讀取文件
總結:Go 中的面向對象
11.10 反射包
12.7 用 defer 關閉文件
9.4 精密計算和 big 包
4.4 變量
6.1 介紹
13.4 自定義包中的錯誤處理和 panicking
12.4 從命令行讀取參數(shù)
9.10 Go 的外部包和項目
8.3 for-range 的配套用法
3.5 格式化代碼
10.4 帶標簽的結構體
7.5 切片的復制與追加
?# 11.3 類型斷言:如何檢測和轉換接口變量的類型
5.4 for 結構
4.8 時間和日期
2.3 在 Linux 上安裝 Go
12 讀寫數(shù)據
6.12 通過內存緩存來提升性能
9.1 標準庫概述
12.6 用切片讀寫文件
10 結構(struct)與方法(method)
8.5 map 的排序
12.9 JSON 數(shù)據格式
13.5 一種用閉包處理錯誤的模式
3.2 編輯器和集成開發(fā)環(huán)境
12.12 Go 中的密碼學
5.2 測試多返回值函數(shù)的錯誤
6.7 將函數(shù)作為參數(shù)
8.2 測試鍵值對是否存在及刪除元素
3.4 構建并運行 Go 程序
2.1 平臺與架構
5.3 switch 結構

6.2 函數(shù)參數(shù)與返回值

函數(shù)能夠接收參數(shù)供自己使用,也可以返回零個或多個值(我們通常把返回多個值稱為返回一組值)。相比與 C、C++、Java 和 C#,多值返回是 Go 的一大特性,為我們判斷一個函數(shù)是否正常執(zhí)行(參考 第 5.2 節(jié))提供了方便。

我們通過 return 關鍵字返回一組值。事實上,任何一個有返回值(單個或多個)的函數(shù)都必須以 returnpanic(參考 第 13 章)結尾。

在函數(shù)塊里面,return 之后的語句都不會執(zhí)行。如果一個函數(shù)需要返回值,那么這個函數(shù)里面的每一個代碼分支(code-path)都要有 return 語句。

問題 6.1:下面的函數(shù)將不會被編譯,為什么呢?大家可以試著糾正過來。

func (st *Stack) Pop() int {
    v := 0
    for ix := len(st) - 1; ix >= 0; ix-- {
        if v = st[ix]; v != 0 {
            st[ix] = 0
            return v
        }
    }
}    

函數(shù)定義時,它的形參一般是有名字的,不過我們也可以定義沒有形參名的函數(shù),只有相應的形參類型,就像這樣:func f(int, int, float64)。

沒有參數(shù)的函數(shù)通常被稱為 niladic 函數(shù)(niladic function),就像 main.main()。

6.2.1 按值傳遞(call by value) 按引用傳遞(call by reference)

Go 默認使用按值傳遞來傳遞參數(shù),也就是傳遞參數(shù)的副本。函數(shù)接收參數(shù)副本之后,在使用變量的過程中可能對副本的值進行更改,但不會影響到原來的變量,比如 Function(arg1)

如果你希望函數(shù)可以直接修改參數(shù)的值,而不是對參數(shù)的副本進行操作,你需要將參數(shù)的地址(變量名前面添加&符號,比如 &variable)傳遞給函數(shù),這就是按引用傳遞,比如 Function(&arg1),此時傳遞給函數(shù)的是一個指針。如果傳遞給函數(shù)的是一個指針,指針的值(一個地址)會被復制,但指針的值所指向的地址上的值不會被復制;我們可以通過這個指針的值來修改這個值所指向的地址上的值。(譯者注:指針也是變量類型,有自己的地址和值,通常指針的值指向一個變量的地址。所以,按引用傳遞也是按值傳遞。

幾乎在任何情況下,傳遞指針(一個32位或者64位的值)的消耗都比傳遞副本來得少。

在函數(shù)調用時,像切片(slice)、字典(map)、接口(interface)、通道(channel)這樣的引用類型都是默認使用引用傳遞(即使沒有顯式的指出指針)。

有些函數(shù)只是完成一個任務,并沒有返回值。我們僅僅是利用了這種函數(shù)的副作用,就像輸出文本到終端,發(fā)送一個郵件或者是記錄一個錯誤等。

但是絕大部分的函數(shù)還是帶有返回值的。

如下,simple_function.go 里的 MultiPly3Nums 函數(shù)帶有三個形參,分別是 a、b、c,還有一個 int 類型的返回值(被注釋的代碼具有和未注釋部分同樣的功能,只是多引入了一個本地變量):

示例 6.2 simple_function.go

package main

import "fmt"

func main() {
    fmt.Printf("Multiply 2 * 5 * 6 = %d\n", MultiPly3Nums(2, 5, 6))
    // var i1 int = MultiPly3Nums(2, 5, 6)
    // fmt.Printf("MultiPly 2 * 5 * 6 = %d\n", i1)
}

func MultiPly3Nums(a int, b int, c int) int {
    // var product int = a * b * c
    // return product
    return a * b * c
}

輸出顯示:

Multiply 2 * 5 * 6 = 60

如果一個函數(shù)需要返回四到五個值,我們可以傳遞一個切片給函數(shù)(如果返回值具有相同類型)或者是傳遞一個結構體(如果返回值具有不同的類型)。因為傳遞一個指針允許直接修改變量的值,消耗也更少。

問題 6.2:

如下的兩個函數(shù)調用有什么不同:

(A) func DoSomething(a *A) {
        b = a
    }

(B) func DoSomething(a A) {
        b = &a
    }

6.2.2 命名的返回值(named return variables)

如下,multiple_return.go 里的函數(shù)帶有一個 int 參數(shù),返回兩個 int 值;其中一個函數(shù)的返回值在函數(shù)調用時就已經被賦予了一個初始零值。

getX2AndX3getX2AndX3_2 兩個函數(shù)演示了如何使用非命名返回值與命名返回值的特性。當需要返回多個非命名返回值時,需要使用 () 把它們括起來,比如 (int, int)。

命名返回值作為結果形參(result parameters)被初始化為相應類型的零值,當需要返回的時候,我們只需要一條簡單的不帶參數(shù)的return語句。需要注意的是,即使只有一個命名返回值,也需要使用 () 括起來(參考 第 6.6 節(jié)的 fibonacci.go 函數(shù))。

示例 6.3 multiple_return.go

package main

import "fmt"

var num int = 10
var numx2, numx3 int

func main() {
    numx2, numx3 = getX2AndX3(num)
    PrintValues()
    numx2, numx3 = getX2AndX3_2(num)
    PrintValues()
}

func PrintValues() {
    fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3)
}

func getX2AndX3(input int) (int, int) {
    return 2 * input, 3 * input
}

func getX2AndX3_2(input int) (x2 int, x3 int) {
    x2 = 2 * input
    x3 = 3 * input
    // return x2, x3
    return
}

輸出結果:

num = 10, 2x num = 20, 3x num = 30    
num = 10, 2x num = 20, 3x num = 30 

警告:

  • return 或 return var 都是可以的。
  • 不過 return var = expression(表達式) 會引發(fā)一個編譯錯誤:syntax error: unexpected =, expecting semicolon or newline or }

即使函數(shù)使用了命名返回值,你依舊可以無視它而返回明確的值。

任何一個非命名返回值(使用非命名返回值是很糟的編程習慣)在 return 語句里面都要明確指出包含返回值的變量或是一個可計算的值(就像上面警告所指出的那樣)。

盡量使用命名返回值:會使代碼更清晰、更簡短,同時更加容易讀懂。

練習 6.1 mult_returnval.go

編寫一個函數(shù),接收兩個整數(shù),然后返回它們的和、積與差。編寫兩個版本,一個是非命名返回值,一個是命名返回值。

練習 6.2 error_returnval.go

編寫一個名字為 MySqrt 的函數(shù),計算一個 float64 類型浮點數(shù)的平方根,如果參數(shù)是一個負數(shù)的話將返回一個錯誤。編寫兩個版本,一個是非命名返回值,一個是命名返回值。

6.2.3 空白符(blank identifier)

空白符用來匹配一些不需要的值,然后丟棄掉,下面的 blank_identifier.go 就是很好的例子。

ThreeValues 是擁有三個返回值的不需要任何參數(shù)的函數(shù),在下面的例子中,我們將第一個與第三個返回值賦給了 i1f1。第二個返回值賦給了空白符 _,然后自動丟棄掉。

示例 6.4 blank_identifier.go

package main

import "fmt"

func main() {
    var i1 int
    var f1 float32
    i1, _, f1 = ThreeValues()
    fmt.Printf("The int: %d, the float: %f \n", i1, f1)
}

func ThreeValues() (int, int, float32) {
    return 5, 6, 7.5
}

輸出結果:

The int: 5, the float: 7.500000

另外一個示例,函數(shù)接收兩個參數(shù),比較它們的大小,然后按小-大的順序返回這兩個數(shù),示例代碼為minmax.go。

示例 6.5 minmax.go

package main

import "fmt"

func main() {
    var min, max int
    min, max = MinMax(78, 65)
    fmt.Printf("Minmium is: %d, Maximum is: %d\n", min, max)
}

func MinMax(a int, b int) (min int, max int) {
    if a < b {
        min = a
        max = b
    } else { // a = b or a < b
        min = b
        max = a
    }
    return
}

輸出結果:

Minimum is: 65, Maximum is 78

6.2.4 改變外部變量(outside variable)

傳遞指針給函數(shù)不但可以節(jié)省內存(因為沒有復制變量的值),而且賦予了函數(shù)直接修改外部變量的能力,所以被修改的變量不再需要使用 return 返回。如下的例子,reply 是一個指向 int 變量的指針,通過這個指針,我們在函數(shù)內修改了這個 int 變量的數(shù)值。

示例 6.6 side_effect.go

package main

import (
    "fmt"
)

// this function changes reply:
func Multiply(a, b int, reply *int) {
    *reply = a * b
}

func main() {
    n := 0
    reply := &n
    Multiply(10, 5, reply)
    fmt.Println("Multiply:", *reply) // Multiply: 50
}

這僅僅是個指導性的例子,當需要在函數(shù)內改變一個占用內存比較大的變量時,性能優(yōu)勢就更加明顯了。然而,如果不小心使用的話,傳遞一個指針很容易引發(fā)一些不確定的事,所以,我們要十分小心那些可以改變外部變量的函數(shù),在必要時,需要添加注釋以便其他人能夠更加清楚的知道函數(shù)里面到底發(fā)生了什么。

鏈接

上一篇:3.5 格式化代碼下一篇:11.9 空接口