概述
我們将用幾節來學習Go語言基礎,本文結構如下:
資料
new 配置設定
構造函數與複合字面
make 配置設定
數組
切片
二維切片
映射
列印
追加
初始化
常量
變量
init 函數
複制
資料
本節包含了 Go 為變量配置設定記憶體的方式,和常用的數組,map兩種資料結構。
Go提供了兩種配置設定方式,即内建函數 new 和 make。
關鍵點:
- make 隻适用于映射、切片和信道且不傳回指針。
- 若要獲得明确的指針, 請使用 new 配置設定記憶體。
new 配置設定
new 函數格式為: new(T)
特點:它傳回一個指針, 該指針指向新配置設定的,類型為 T 的零值
内建函數 new 是個用來配置設定記憶體的内建函數, 但與其它語言中的同名函數不同,它不會初始化記憶體,隻會将記憶體置零。
Go 的 new比于java的情形是,java可以通過 new 執行構造來初始化一個對象,而Go不能初始化(賦初值),它隻能置為”零值“
也就是說,new(T) 會為類型為 T 的新項配置設定已置零的記憶體空間, 并傳回它的位址,也就是一個類型為 *T 的值。用Go的術語來說,
它傳回一個指針, 該指針指向新配置設定的,類型為 T 的零值
。
這樣的設計,使得無需像Java那樣面對不同對象的豐富多彩的構造函數和參數。
既然 new 傳回的記憶體已置零,就不必進一步初始化了,使用者隻需用 new 建立一個新的對象就能正常工作。
例如:
- bytes.Buffer 的文檔中提到“零值的 Buffer 就是已準備就緒的緩沖區。"
- sync.Mutex 并沒有顯式的構造函數或 Init 方法, 而是零值的 sync.Mutex 就已經被定義為已解鎖的互斥鎖了。
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
如上的兩種方式,都會配置設定好記憶體空間,而類型是不同的。
構造函數與複合字面
有些場景下,仍然需要一個初始化構造函數,就像 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
}
複制
注意 File{fd, name, nil, 0} 這樣的寫法就是
複合字面
的寫法。該表達式在每次求值時都會建立新的執行個體。
複合字面的字段必須
按順序全部列出
。但如果以
字段:值
對的形式明确地标出元素,初始化字段時就可以按任何順序出現,未給出的字段值将賦予零值。 是以,我們可以用如下形式:
return &File{fd: fd, name: name}
複制
make 配置設定
内建函數 make 的格式為: make(T, args)
特點:它隻用于建立切片、映射和信道,并傳回類型為 T(而非 *T)的一個已初始化 (而非置零)的值。
切片、映射和信道 本質上為引用資料類型,在使用前必須初始化。 例如,切片是一個具有三項内容的描述符,包含一個指向(數組内部)資料的指針、長度以及容量, 在這三項被初始化之前,該切片為 nil。
對于切片、映射和信道,make 用于初始化其内部的資料結構并準備好将要使用的值。
例如:
make([]int, 10, 100) 配置設定一個具有100個 int 的數組空間,接着建立一個長度為10, 容量為100并指向該數組中前10個元素的切片結構
new([]int) 會傳回一個指向新配置設定的,已置零的切片結構, 即一個指向 nil 切片值的指針。
下面的例子闡明了 new 和 make 之間的差別:
var p *[]int = new([]int) // 配置設定切片結構;*p == nil;基本沒用
var v []int = make([]int, 100) // 切片 v 現在引用了一個具有 100 個 int 元素的新數組
// 沒必要的複雜:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// 習慣用法:
v := make([]int, 100)
複制
再次說明關鍵點:
- make 隻适用于映射、切片和信道且不傳回指針。
- 若要獲得明确的指針, 請使用 new 配置設定記憶體。
數組
在規劃記憶體布局時,數組是非常有用的,有時還能避免過多的記憶體配置設定, 在Go中,數組主要用作切片的構件,在建構切片時使用。
數組在Go和C中的主要差別。在Go中:
- 數組是值。将一個數組賦予另一個數組會複制其所有元素。
- 若将某個數組傳入某個函數,它将接收到該數組的一份副本而非指針。
- 數組的大小是其類型的一部分。類型 [10]int 和 [20]int 是不同的。
數組為值的屬性很有用,但代價高昂;若你想要C那樣的行為和效率,你可以傳遞一個指向該數組的指針。
在 Go 中,更習慣的的用法是使用 切片。
切片
切片通過對數組進行封裝,為有序列的資料提供了更通用、強大而友善的方式。
除了矩陣變換這類需要明确次元的情況外,Go中的大部分數組程式設計都是通過切片來完成的。
切片儲存了對底層數組的引用,若你将某個切片賦予另一個切片,它們會引用同一個數組。 若某個函數将一個切片作為參數傳入,則它對該切片元素的修改對調用者而言同樣可見, 這可以了解為傳遞了底層數組的指針。
修改長度:隻要切片不超出底層數組的限制,它的長度就是可變的,隻需産生新的切片再次指向自身變量即可。
切片的長度:
len(切片)
複制
切片的容量可通過内建函數 cap 獲得,它将給出該切片可取得的最大長度。函數為:
cap(切片)
複制
若資料超出其容量,則會重新配置設定該切片。傳回值即為所得的切片。
向切片追加東西的很常用,是以有專門的内建函數 append。
一般情況下,如果我們要寫一個 append 方法的話,最終傳回值必須傳回切片。示例:
func Append(slice, data[]byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // 重新配置設定
// 為了後面的增長,需配置設定兩份。
newSlice := make([]byte, (l+len(data))*2)
// copy 函數是預聲明的,且可用于任何切片類型。
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
for i, c := range data {
slice[l+i] = c
}
return slice
}
複制
如上,輸入參數是切片和插入的元素值,傳回值是切片,注意切片的長度會發生變化。
因為盡管 Append 可修改 切片 的元素,但切片自身(其運作時資料結構包含指針、長度和容量)是通過值傳遞的。
二維切片
要建立等價的二維數組或切片,就必須定義一個數組的數組, 或切片的切片,示例:
type Transform [3][3]float64 // 一個 3x3 的數組,其實是包含多個數組的一個數組。
type LinesOfText [][]byte // 包含多個位元組切片的一個切片。
複制
每行都有其自己的長度:
由于切片長度是可變的,是以其内部可能擁有多個不同長度的切片。
映射 (map)
映射 是Go中 資料結構中的 map結構實作,即 key: value的形式存儲。
映射的值可以是各種類型。
映射的鍵可以是整數、浮點數、複數、字元串、指針、接口等。
映射的鍵(或者叫索引)可以是任何相等性操作符支援的類型, 如整數、浮點數、複數、字元串、指針、接口(隻要其動态類型支援相等性判斷)、結構以及數組。 切片不能用作映射鍵,因為它們的相等性還未定義。與切片一樣,映射也是引用類型。
如果将映射作為參數傳入函數中,并更改了該映射的内容,則此修改對調用者同樣可見。
映射可使用一般的複合字面文法進行建構,其鍵-值對使用逗号分隔,有點像JSON:
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。
判斷某個值是否存在:
seconds, ok = timeZone[tz]
複制
上面是慣用的 "逗号 ok” 法:
- 若 tz 存在, seconds 就會被賦予适當的值,且 ok 會被置為 true; - 若不存在,seconds 則會被置為零,而 ok 會被置為 false。
若僅需判斷映射中是否存在某項而不關心實際的值,可使用空白辨別符
_
來代替該值的一般變量。
_, present := timeZone[tz]
複制
要删除映射中的某項,可使用内建函數
delete
。即便對應的鍵不在該映射中,此操作也是安全的。
delete(timeZone, "PDT")
複制
列印
Go的格式化列印風格和C的 printf 類似,但卻更加豐富而通用。 這些函數位于 fmt 包中,且函數名首字母均為大寫:如 fmt.Printf、fmt.Fprintf,fmt.Sprintf 等。
看例子:
// 以f 結尾的這幾個,傳入格式化字元串作為參數, 不換行
fmt.Printf("hello, %v \n","zhang3")
fmt.Fprintf(os.Stdout,"hello, %v \n","zhang3")
str := fmt.Sprintf("hello, %v \n","zhang3")
//下面這幾個,會換行
fmt.Println(str)
// 注意下面這個,會自動在元素間插入空格
fmt.Fprintln(os.Stdout,"f1","f2","f3")
複制
Sprintf 用于構造字元串: 字元串函數(Sprintf 等)會傳回一個字元串,而不是寫入到資料流中。
Fprint 用于寫入到各種流中:fmt.Fprint 一類的格式化列印函數可接受任何實作了 io.Writer 接口的對象作為第一個實參;比如 os.Stdout 與 os.Stderr 。
下面對 Printf 支援的格式化的字元做一些說明:
-- 格式: %d
像 %d 不接受表示符号或大小的标記, 會根據實際的類型來決定這些屬性。
var x uint64 = 1<<64 - 1 // x 是無符号整數, 下面的 int64(x) 轉換為有符合整數
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
複制
将列印
18446744073709551615 ffffffffffffffff; -1 -1
複制
-- 格式: %v
%v 可了解為 實際的 value。
它還能列印任意值,甚至包括數組、結構體和映射。
fmt.Printf("%v\n", timeZone) // 或隻用 fmt.Println(timeZone)
複制
這會輸出
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
複制
%+v 和 %#v
當列印結構體時,格式 %+v 會帶上每個字段的字段名,而格式 %#v 會帶上類型。
type T struct {
a int
b float64
c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
複制
将列印
&{7 -2.35 abc def} // 請注意其中的&符号
&{a:7 b:-2.35 c:abc def} // 有了字段名
&main.T{a:7, b:-2.35, c:"abc\tdef"} //有了類型
複制
-- 格式:%q
當遇到 string 或 []byte 值時, 可使用 %q 産生帶引号的字元串;而格式 %#q 會盡可能使用反引号。
--格式:%x
%x 還可用于字元串、位元組數組以及整數,并生成一個很長的十六進制字元串, 而帶空格的格式(% x)還會在位元組之間插入空格。
--格式: %T
它會列印某個值的類型.
fmt.Printf("%T\n", timeZone)
複制
會列印
map[string] int
複制
-- 為結構圖自定義輸出
類似 java 中的 toString(),對結構圖自定義類型的預設格式,隻需為該類型定義一個具有 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"
複制
-- 任意數量的
Printf 的簽名為其最後的實參使用了 ...interface{} 類型,這樣格式的後面就能出現任意數量,任意類型的形參了。
func Printf(format string, v ...interface{}) (n int, err error) {
複制
在 Printf 函數的實作中,v 看起來更像是 []interface{} 類型的變量,但如果将它傳遞到另一個變參函數中,它就像是正常實參清單了。實際上,它直接将其實參傳遞給 fmt.Sprintln 進行實際的格式化。
// Println 通過 fmt.Println 的方式将日志列印到标準記錄器。
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // Output 接受形參 (int, string)
}
複制
注意上面的 ...interface{} 和 v... 的寫法。
追加 ( append 函數 說明 )
append 函數的簽名就像這樣:
func append(slice []T, 元素 ...T) []T
複制
其中的 T 為任意給定類型的占位符。實際上,你無法編寫一個類型 T 由調用者決定的函數。這也就是為何 append 為内建函數的原因:它需要編譯器的支援。
append 會在切片末尾追加元素并傳回結果。我們必須傳回結果, 原因是,底層數組可能會被改變(注意數組的長度是類型的一部分)。
以下簡單的例子
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
複制
将列印
[1 2 3 4 5 6]
複制
将一個切片追加到另一個切片很簡單:在調用的地方使用 ...
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
複制
如果沒有 ...,它就會由于類型錯誤而無法編譯,因為 y 不是 int 類型的。三個點符号 “
...
” 的作用有點像“ 展開 ” 的作用,即将 y這個切片的元素放到了這裡。
初始化
GO 的huaGo的初始化很強大,在初始化過程中,不僅可以建構複雜的結構,還能正确處理不同包對象間的初始化順序。
常量
常量在編譯時被建立,即便函數中定義的局部變量也一樣。
常量隻能是數字、字元(符文)、字元串或布爾值。
由于編譯時的限制, 定義它們的表達式必須是可被編譯器求值的常量表達式。例如 1<<3 就是一個常量表達式。
枚舉常量
枚舉常量使用枚舉器 iota 建立。由于 iota 可為表達式的一部分,而表達式可以被隐式地重複,這樣也就更容易建構複雜的值的集合了。
type ByteSize float64
const (
// 通過賦予空白辨別符來忽略第一個值
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
複制
變量
變量的初始化與常量類似,但其初始值也可以是在運作時才被計算的一般表達式。
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
複制
init 函數
每個源檔案都可以通過定義自己的無參數 init 函數來設定一些必要的狀态。格式為:
func init() {
...
}
複制
而 init 方法執行結束,就意味着初始化結束了:隻有該包中的所有變量聲明都通過它們的初始化器求值後 init 才會被調用, 而那些 init 隻有在所有已導入的包都被初始化後才會被求值。
init 函數還常被用在程式真正開始執行前,檢驗或校正程式的狀态。示例:
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath 可通過指令行中的 --gopath 标記覆寫掉。
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}
複制