## 5.8\. 數據
### 5.8.1\. new()分配
Go 有兩個分配原語,new() 和 make() 。它們做法不同,也用作不同類型上。有點亂但規則簡單。我們先談談 new() 。它是個內部函數,本質上和其它語言的同類一樣:new(T)分配一塊清零的存儲空間給類型 T 的新項并返回其地址,一個類型 *T 的值。 用 Go 的術語,它返回一個類型 T 的新分配的零值。
因為 new() 返回的內存清零, 可以用來安排使用零值的物件而不需再初始化。亦即數據結構的用戶可以直接用 new() 生成一個并馬上使用。例如, bytes.Buffer 的文檔指出“零值的 Buffer 為空并可用”。同[http://code.google.com/p/ac-me/](http://code.google.com/p/ac-me/) 61理,sync.Mutex 沒有明確的架構函數或 init 方法。 而是,一個sync.Mutex 的零值定義為開鎖的互斥。
零值有用,這個特性可以順延。考慮下面的聲明。
```
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
```
類型 SyncBuffer 的值在分配或者聲明后立即可用。下例,p 和 v 無需多余的安排已可以正確使用了。
```
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
```
### 5.8.2\. 構造和結構初始化
有時零值不夠好,有必要使用一個初始化架構函數,如下面從 os 包引出的例子。
```
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
```
這里有很多注模。我們可用組合字面簡化之,它是個每次求值即生成新實例的表達式。
```
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
```
注意返回局部變量的地址是完全 OK 的;變量對應的存儲空間在函數返回后仍然存在。實際上,取一個組合字面的地址使每次它求值時都生成一個新實例,因此我們可以把最后兩行合起來。
```
return &File{fd, name, nil, 0}
```
組合字面的域必須按順序給出并全部出現。可是,明確的用域:值對兒標記元素,初始化可用任意順序,未出現的對應著零值。所以我們可以講
```
return &File{fd: fd, name: name}
```
特別的,如果一個組合字面一個域也沒有,它生成此類型的零值。表達式 new(File) 和 &File{} 是等價的。
組合字面也可以生成數組、切片和映射,其域為合適的下標或映射鍵。下例中,無論 Enone Eio 和 Einval 是什么值都可以,只要它們是不同的。
```
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
```
### 5.8.3\. make()分配
回到分配。內部函數 make(T, args) 的服務目的和 new(T) 不同。它只生成切片,映射和信道,并返回一個初始化的(不是零)的,type T的,不是 *T 的值。這種區分的原因是,這三種類型,揭開蓋子,底下引用的數據結構必須在用前初始化。比如切片是一個三項的描述符,包含數據指針(數組內),長度,和容量;在這些項初始化前,切片為 nil 。對于切片、映射和信道,make 初始化內部數據結構,并準備要用的值。例如,
```
make([]int, 10, 100)
```
分配一個 100 個整數的數組,然后生成一個切片結構,長度為10,容量是100的指向此數組的首10項。(生成切片時,容量可以不寫;詳見切片一節。)對應的,new([]int) 返回一個新分配的,清零的切片結構,亦即,一個 nil 切片值的指針。
下面的例子展示了 new() 和 make() 的不同。
```
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // v now refers to a new array of 100 ints
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)
```
記住 make() 只用于映射、切片和信道,不返回指針。要明確的得到指針用 new() 分配。
### 5.8.4\. 數組
數組用于安排詳細的內存布局,還有助于避免分配,但其主要作為切片的構件,即下節的主題。這里先講幾句打個底兒。
Go 和 C 的數組的主要不同在于:
* 數組為值。數組賦值給另一數組拷貝其全部元素。
* 特別是,如果你傳遞數組給一個函數,它受到此數組的拷貝,不是指針。
* 數組的尺寸是其類型的一部分。[10]int 和 [20]int 是完全不同的類型。
值的屬性可用但昂貴;如你所需的是類似 C 的行為和效率,你可以傳遞一個指針給數組。
```
func Sum(a *[3]float) (sum float) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
```
即便如此也不是地道的 Go 風格。切片才是。
### 5.8.5\. Slices 切片
切片包裝數組,給數據系列一個通用、強力、方便的界面。除了像變換矩陣那種要求明確尺寸的情況,絕大部分的數組編程在 Go 里使用切片、而不是簡單的數組。
切片是引用類型,即如果賦值切片給另一個切片,它們都指向同一底層數組。例如,如果某函數取切片參量,對其元素的改動會顯現在調用者中,類似于傳遞一個底層數組的指針。因此 Read 函數可以接受切片參量,而不需指針和計數;切片的長度決定了可讀數據的上限。這里是 os 包的 File 型的 Read 方法的簽名:
```
func (file *File) Read(buf []byte) (n int, err os.Error)
```
此方法返回讀入字節數和可能的錯誤值。要讀入一個大的緩沖 b 的首32字節, 切片(動詞)緩沖。
```
n, err := f.Read(buf[0:32])
```
這種切片常用且高效。實際上,先不管效率,此片段也可讀緩沖的首32字節。
```
var n int
var err os.Error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1]) // Read one byte.
if nbytes == 0 || e != nil {
err = e
break
}
n += nbytes
}
```
只要還在底層數組的限制內,切片的長度可以改變,只需賦值自己。切片的容量,可用內部函數 cap 取得,給出此切片可用的最大長度。下面的函數給切片添值。如果數據超過容量,切片重新分配,返回結果切片。此函數利用了 len 和 cap 對 nil 切片合法、返回0的事實。
```
func Append(slice, data[]byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// Copy data (could use bytes.Copy()).
for i, c := range slice {
newSlice[i] = c
}
slice = newSlice
}
slice = slice[0:l+len(data)]
for i, c := range data {
slice[l+i] = c
}
return slice
}
```
我們必須返回切片,因為盡管 Append 可以改變 slice 的元素, 切片自身(持有指針、長度和容量的運行態數據結構)是值傳遞的。添加切片的主意很有用,因此由內置函數 append 實現。要理解此函數的設計,我們需要多一些信息,所以稍后再講。
### 5.8.6\. Maps 字典
映射提供了一個方便強力的內部數據結構,用來聯合不同的類型。鍵可以是任何定義了相等操作符的類型,如整型,浮點型,字串,指針,界面(只要其動態類型支持相等)。結構,數組和切片不可用作映射鍵,因為其類型未定義相等。類似切片,映射是引用類型。如果你傳遞映射給某函數,對映射的內容的改動顯現給調用者。
映射的生成使用平常的冒號隔開的鍵值伴組合字面句法,所以很容易初始化時建好它們。
```
var timeZone = map[string] int {
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
```
賦值和獲取映射值語法上就像數組,只是下標不需是整型。
```
offset := timeZone["EST"]
```
試圖獲取不存在的鍵的映射值返回對應條目類型的零值。例如,如果映射包含整型數,查找不存在的鍵返回0。
有時你需區分不在鍵和零值。 是沒有 “UTC” 的條目,還是因為其值為零?你可以用多值賦值的形式加以區分。
```
var seconds int
var ok bool
seconds, ok = timeZone[tz]
```
道理很明顯,此習語稱為“逗號ok”。此例中,如果 tz 存在,seconds 相應賦值,ok為真;否則,seconds 為0,ok為假。下面的函數加上了中意的出錯報告:
```
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Stderr("unknown time zone", tz)
return 0
}
```
要檢查映射的存在,又不想管實際值,你可以用空白標識,即下劃線( _ )。空白標識可以賦值或聲明為任意類型的任意值,會被無害的丟棄。如只要測試映射是否存在, 在平常變量的地方使用空白標識即可。
```
_, present := timeZone[tz]
```
要刪除映射條目,翻轉多值賦值,在右邊多放個布爾;如果布爾為假,條目被刪。即便鍵已經不再了,這樣做也是安全的。
```
timeZone["PDT"] = 0, false // Now on Standard Time
```
### 5.8.7\. 打印
Go 的排版打印風格類似 C 的 printf 族但更豐富更通用。這些函數活在 fmt 包里,叫大寫的名字:fmt.Printf,fmt.Fprintf, fmt.Sprintf 等等。字串函數(Sprintf 等)返回字串,而不是填充給定的緩沖。
你不需給出排版字串。對應每個 Printf,Fprintf 和 Sprintf 都有另一對函數。例如 Print 和 Println。 它們不需排版字串,而是用每個參量默認的格式。Println 版本還會在參量間加入空格和輸出新行,而 Print 版本只當操作數的兩邊都不是字串時才添加空格。下例每行的輸出都是一樣的:
```
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println(fmt.Sprint("Hello ", 23))
```
如《輔導》里所講,fmt.Fprint 和伙伴們的第一個參量可以是任何實現 io.Writer 界面的物件。變量 os.Stdout 和 os.Stderr 是熟悉的實例。
從此事情開始偏離 C 了。首先,數字格式如 %d 沒有正負和尺寸的標記;打印例程使用參量的類型決定這些屬性。
```
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
```
打印出:
```
18446744073709551615 ffffffffffffffff; -1 -1
```
如果你只需默認的轉換,例如整數用十進制,你可以用全拿格式 %v(代表 value);結果和Print 與 Println 打印的完全一樣。再有,此格式可打印任意值,包括數組,結構和映射。這里是上節定義的時區映射的打印語句。
```
fmt.Printf("%v\n", timeZone) // or just fmt.Println(timeZone)
```
打印出:
```
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
```
當然,映射的鍵會以任意順序輸出。打印結構時,改進的格式 %+v 用結構的域名注釋,對任意值格式 %#v 打印出完整的 Go 句法。
```
type T struct {
a int
b float
c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
```
打印出:
```
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}
```
(注意和號&)。引號括起的字串也可以 %q 用在 string 或 []byte 類型的值上,對應的格式 %#q 如果可能則使用反引號。還有,%x 可用于字串、字節數組和整型,得到長的十六進制串,有空格的格式(% x)會在字節間加空格。
另一好用的格式是 %T,打印某值的類型。
```
fmt.Printf("%T\n", timeZone)
```
打印出:
```
map[string] int
```
如果你要控制某定制類型的默認格式, 只需在其類型上定義方法String() string。對我們簡單的類型 T,可以是:
```
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)
```
來打印
```
7/-2.35/"abc\tdef"
```
我們的String() 方法可以調用 Sprint,因為打印例程是完全可以重入可以遞歸的。我們可以更進一步,把一個打印例程的參量直接傳遞給另一打印例程。 Printf 的簽名的首參量使用類型 ...interface{},來指定任意數量任意類型的參量可以出現在格式字串的后面。
```
func Printf(format string, v ...) (n int, errno os.Error) {
```
Printf 函數中,v 像是一個 []interface{} 類的變量。但如果把它傳遞給另一個多維函數,它就像一列普通的參量。這里是我們上面用過的log.Println 的實現。它把自己的參量直接傳遞給 fmt.Sprintln 來實際打印。
```
// Stderr is a helper function for easy logging to stderr. It is analogous to Fprint(os.Stderr).
func Stderr(v ...) {
stderr.Output(2, fmt.Sprintln(v)) // Output takes parameters (int, string)
}
```
我們在 Sprintln 的調用的 v 后寫 ... 告訴編譯器把 v 作為一列參量;否則它只是傳遞一個單一的切片參量。
還有很多打印的內容我們還沒講,細節可參考 godoc 的 fmt 包的文檔。
順便提一句, ... 參量可以是任意給定的類型,例如,...int 在 min 函數里可以選一列整數的最小值。
```
func Min(a ...int) int {
min := int(^uint(0) >> 1) // largest int
for _, i := range a {
if i < min {
min = i
}
}
return min
}
```
### 5.8.8\. Append
現在我們解釋 append 的設計。append 的簽名和上面我們定制的Append 函數不同。大體上是:
```
func append(slice []T, elements...T) []T
```
T 替代的是任意類型。 實際中你不能寫 Go 的函數由調用者決定 T 的類型,所以 append 內置:它需要編譯器的支持。
append 所做的是在切片尾添加元素并返回結果。結果需要返回因為,正如我們手寫的 Append,底層的數組可能更改。下面簡單的例子:
```
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
```
打印 [1 2 3 4 5](https://golang-china.googlecode.com/svn/trunk/Chinese/golang.org/6)。所以 append 有點像 Printf 收集任意數量的參量。
但如何像我們 Append 一樣給切片添加切片呢?容易:使用 ... 在調用的地方,正如我們上面我們調用 Output。 下例產生如上同樣的輸出:
```
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
```
沒有 ... 將不能編譯,因為類型錯誤; y 不是 int 類型。
- 1. 關于本文
- 2. Go語言簡介
- 3. 安裝go環境
- 3.1. 簡介
- 3.2. 安裝C語言工具
- 3.3. 安裝Mercurial
- 3.4. 獲取代碼
- 3.5. 安裝Go
- 3.6. 編寫程序
- 3.7. 進一步學習
- 3.8. 更新go到新版本
- 3.9. 社區資源
- 3.10. 環境變量
- 4. Go語言入門
- 4.1. 簡介
- 4.2. Hello,世界
- 4.3. 分號(Semicolons)
- 4.4. 編譯
- 4.5. Echo
- 4.6. 類型簡介
- 4.7. 申請內存
- 4.8. 常量
- 4.9. I/O包
- 4.10. Rotting cats
- 4.11. Sorting
- 4.12. 打印輸出
- 4.13. 生成素數
- 4.14. Multiplexing
- 5. Effective Go
- 5.1. 簡介
- 5.2. 格式化
- 5.3. 注釋
- 5.4. 命名
- 5.5. 分號
- 5.6. 控制流
- 5.7. 函數
- 5.8. 數據
- 5.9. 初始化
- 5.10. 方法
- 5.11. 接口和其他類型
- 5.12. 內置
- 5.13. 并發
- 5.14. 錯誤處理
- 5.15. Web服務器
- 6. 如何編寫Go程序
- 6.1. 簡介
- 6.2. 社區資源
- 6.3. 新建一個包
- 6.4. 測試
- 6.5. 一個帶測試的演示包
- 7. Codelab: 編寫Web程序
- 7.1. 簡介
- 7.2. 開始
- 7.3. 數據結構
- 7.4. 使用http包
- 7.5. 基于http提供wiki頁面
- 7.6. 編輯頁面
- 7.7. template包
- 7.8. 處理不存在的頁面
- 7.9. 儲存頁面
- 7.10. 錯誤處理
- 7.11. 模板緩存
- 7.12. 驗證
- 7.13. 函數文本和閉包
- 7.14. 試試!
- 7.15. 其他任務
- 8. 針對C++程序員指南
- 8.1. 概念差異
- 8.2. 語法
- 8.3. 常量
- 8.4. Slices(切片)
- 8.5. 構造值對象
- 8.6. Interfaces(接口)
- 8.7. Goroutines
- 8.8. Channels(管道)
- 9. 內存模型
- 9.1. 簡介
- 9.2. Happens Before
- 9.3. 同步(Synchronization)
- 9.4. 錯誤的同步方式
- 10. 附錄
- 10.1. 命令行工具
- 10.2. 視頻和講座
- 10.3. Release History
- 10.4. Go Roadmap
- 10.5. 相關資源