2015-12-26 07:42:56 +00:00
### 2.3.2. 指針
2015-12-09 07:45:11 +00:00
2015-12-26 12:05:30 +00:00
一個變量對應一個保存了變量對應類型值的內存空間。普通變量在聲明語句創建時被綁定到一個變量名, 比如叫x的變量, 但是還有很多變量始終以表達式方式引入, 例如x[i]或x.f變量。所有這些表達式一般都是讀取一個變量的值, 除非它們是出現在賦值語句的左邊, 這種時候是給對應變量賦予一個新的值。
2015-12-09 07:45:11 +00:00
2015-12-26 07:42:56 +00:00
一個指針的值是另一個變量的地址。一個指針對應變量在內存中的存儲位置。併不是每一個值都會有一個內存地址,但是對於每一個變量必然有對應的內存地址。通過指針,我們可以直接讀或更新對應變量的值,而不需要知道該變量的名字(如果變量有名字的話)。
2015-12-09 07:45:11 +00:00
2015-12-26 12:05:30 +00:00
如果用“var x int”聲明語句聲明一個x變量, 那麽& x表達式( 取x變量的內存地址) 將産生一個指向該整數變量的指針, 指針對應的數據類型是`*int`, 指針被稱之爲“指向int類型的指針”。如果指針名字爲p, 那麽可以説“p指針指向變量x”, 或者説“p指針保存了x變量的內存地址”。同時`*p`表達式對應p指針指向的變量的值。一般`*p`表達式讀取指針指向的變量的值, 這里爲int類型的值, 同時因爲`*p`對應一個變量,所以該表達式也可以出現在賦值語句的左邊,表示更新指針所指向的變量的值。
2015-12-09 07:45:11 +00:00
```Go
x := 1
p := & x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 2
fmt.Println(x) // "2"
```
2015-12-26 07:42:56 +00:00
對於聚合類型每個成員——比如結構體的每個字段、或者是數組的每個元素——也都是對應一個變量,因此可以被取地址。
2015-12-09 07:45:11 +00:00
2016-01-18 03:22:04 +00:00
變量有時候被稱爲可尋址的值。卽使變量由表達式臨時生成,那麽表達式也必須能接受`& `取地址操作。
2015-12-09 07:45:11 +00:00
2015-12-26 12:05:30 +00:00
任何類型的指針的零值都是nil。如果`p != nil`測試爲眞, 那麽p是指向某個有效變量。指針之間也是可以進行相等測試的, 隻有當它們指向同一個變量或全部是nil時才相等。
2015-12-09 07:45:11 +00:00
```Go
var x, y int
fmt.Println(& x == & x, & x == & y, & x == nil) // "true false false"
```
2015-12-26 07:42:56 +00:00
在Go語言中, 返迴函數中局部變量的地址也是安全的。例如下面的代碼, 調用f函數時創建局部變量v, 在局部變量地址被返迴之後依然有效, 因爲指針p依然引用這個變量。
2015-12-09 07:45:11 +00:00
```Go
var p = f()
func f() *int {
v := 1
return & v
}
```
2015-12-26 07:42:56 +00:00
每次調用f函數都將返迴不同的結果:
2015-12-09 07:45:11 +00:00
```Go
fmt.Println(f() == f()) // "false"
```
2015-12-26 07:42:56 +00:00
因爲指針包含了一個變量的地址, 因此如果將指針作爲參數調用函數, 那將可以在函數中通過該指針來更新變量的值。例如下面這個例子就是通過指針來更新變量的值, 然後返迴更新後的值, 可用在一個表達式中( 譯註: 這是對C語言中`++v`操作的模擬, 這里隻是爲了説明指針的用法, incr函數模擬的做法併不推薦) :
2015-12-09 07:45:11 +00:00
```Go
func incr(p *int) int {
2015-12-26 07:42:56 +00:00
*p++ // 非常重要: 隻是增加p指向的變量的值, 併不改變p指針! ! !
2015-12-09 07:45:11 +00:00
return *p
}
v := 1
incr(& v) // side effect: v is now 2
fmt.Println(incr(& v)) // "3" (and v is 3)
```
2016-01-18 03:22:04 +00:00
每次我們對一個變量取地址,或者複製指針,我們都是爲原變量創建了新的别名。例如,`*p`就是是 變量v的别名。指針特别有價值的地方在於我們可以不用名字而訪問一個變量, 但是這是一把雙刃劍: 要找到一個變量的所有訪問者併不容易, 我們必須知道變量全部的别名( 譯註: 這是Go語言的垃圾迴收器所做的工作) 。不僅僅是指針會創建别名, 很多其他引用類型也會創建别名, 例如slice、map和chan, 甚至結構體、數組和接口都會創建所引用變量的别名。
2015-12-09 07:45:11 +00:00
2015-12-26 07:42:56 +00:00
指針是實現標準庫中flag包的關鍵技術, 它使用命令行參數來設置對應變量的值, 而這些對應命令行標誌參數的變量可能會零散分布在整個程序中。爲了説明這一點, 在早些的echo版本中, 就包含了兩個可選的命令行參數: `-n`用於忽略行尾的換行符,`-s sep`用於指定分隔字符( 默認是空格) 。下面這是第四個版本, 對應包路徑爲gopl.io/ch2/echo4。
2015-12-09 07:45:11 +00:00
2016-01-20 15:16:19 +00:00
< u > < i > gopl.io/ch2/echo4< / i > < / u >
2015-12-09 07:45:11 +00:00
```Go
// Echo4 prints its command-line arguments.
package main
import (
"flag"
"fmt"
"strings"
)
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")
func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}
```
2016-01-18 03:22:04 +00:00
調用flag.Bool函數會創建一個新的對應布爾型標誌參數的變量。它有三個屬性: 第一個是的命令行標誌參數的名字“n”, 然後是該標誌參數的默認值( 這里是false) , 最後是該標誌參數對應的描述信息。如果用戶在命令行輸入了一個無效的標誌參數, 或者輸入`-h`或`-help`參數, 那麽將打印所有標誌參數的名字、默認值和描述信息。類似的, 調用flag.String函數將於創建一個對應字符串類型的標誌參數變量, 同樣包含命令行標誌參數對應的參數名、默認值、和描述信息。程序中的`sep`和`n`變量分别是指向對應命令行標誌參數變量的指針,因此必須用`*sep`和`*n`形式的指針語法間接引用它們。
2015-12-09 07:45:11 +00:00
2016-01-18 03:22:04 +00:00
當程序運行時, 必須在使用標誌參數對應的變量之前調用先flag.Parse函數, 用於更新每個標誌參數對應變量的值( 之前是默認值) 。對於非標誌參數的普通命令行參數可以通過調用flag.Args()函數來訪問, 返迴值對應對應一個字符串類型的slice。如果在flag.Parse函數解析命令行參數時遇到錯誤, 默認將打印相關的提示信息, 然後調用os.Exit(2)終止程序。
2015-12-09 07:45:11 +00:00
2015-12-26 07:42:56 +00:00
讓我們運行一些echo測試用例:
2015-12-09 07:45:11 +00:00
```
$ go build gopl.io/ch2/echo4
$ ./echo4 a bc def
a bc def
$ ./echo4 -s / a bc def
a/bc/def
$ ./echo4 -n a bc def
a bc def$
$ ./echo4 -help
Usage of ./echo4:
-n omit trailing newline
-s string
separator (default " ")
```