概述
我們將用幾節(jié)來學(xué)習(xí)Go語言基礎(chǔ)炭晒,本文結(jié)構(gòu)如下:
數(shù)據(jù)
new 分配
構(gòu)造函數(shù)與復(fù)合字面
make 分配
數(shù)組
切片
二維切片
映射
打印
追加
初始化
常量
變量
init 函數(shù)
數(shù)據(jù)
本節(jié)包含了 Go 為變量分配內(nèi)存的方式执庐,和常用的數(shù)組贰镣,map兩種數(shù)據(jù)結(jié)構(gòu)。
Go提供了兩種分配方式孽惰,即內(nèi)建函數(shù) new 和 make晚岭。
關(guān)鍵點:
- make 只適用于映射、切片和信道且不返回指針灰瞻。
- 若要獲得明確的指針腥例, 請使用 new 分配內(nèi)存辅甥。
new 分配
new 函數(shù)格式為: new(T)
特點:它返回一個指針酝润, 該指針指向新分配的,類型為 T 的零值
內(nèi)建函數(shù) new 是個用來分配內(nèi)存的內(nèi)建函數(shù)璃弄, 但與其它語言中的同名函數(shù)不同要销,它不會初始化內(nèi)存,只會將內(nèi)存置零夏块。
Go 的 new比于java的情形是疏咐,java可以通過 new 執(zhí)行構(gòu)造來初始化一個對象,而Go不能初始化(賦初值)脐供,它只能置為”零值“
也就是說浑塞,new(T) 會為類型為 T 的新項分配已置零的內(nèi)存空間, 并返回它的地址政己,也就是一個類型為 *T 的值酌壕。用Go的術(shù)語來說,它返回一個指針, 該指針指向新分配的卵牍,類型為 T 的零值
果港。
這樣的設(shè)計,使得無需像Java那樣面對不同對象的豐富多彩的構(gòu)造函數(shù)和參數(shù)糊昙。
既然 new 返回的內(nèi)存已置零辛掠,就不必進一步初始化了,使用者只需用 new 創(chuàng)建一個新的對象就能正常工作释牺。
例如:
- bytes.Buffer 的文檔中提到“零值的 Buffer 就是已準(zhǔn)備就緒的緩沖區(qū)萝衩。"
- sync.Mutex 并沒有顯式的構(gòu)造函數(shù)或 Init 方法, 而是零值的 sync.Mutex 就已經(jīng)被定義為已解鎖的互斥鎖了没咙。
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
如上的兩種方式欠气,都會分配好內(nèi)存空間,而類型是不同的镜撩。
構(gòu)造函數(shù)與復(fù)合字面
有些場景下预柒,仍然需要一個初始化構(gòu)造函數(shù),就像 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
}
上面的代碼過于冗長袁梗。我們可通過復(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} 這樣的寫法就是 復(fù)合字面
的寫法宜鸯。該表達式在每次求值時都會創(chuàng)建新的實例。
復(fù)合字面的字段必須按順序全部列出
遮怜。但如果以 字段:值
對的形式明確地標(biāo)出元素淋袖,初始化字段時就可以按任何順序出現(xiàn),未給出的字段值將賦予零值锯梁。 因此即碗,我們可以用如下形式:
return &File{fd: fd, name: name}
make 分配
內(nèi)建函數(shù) make 的格式為: make(T, args)
特點:它只用于創(chuàng)建切片、映射和信道陌凳,并返回類型為 T(而非 *T)的一個已初始化 (而非置零)的值剥懒。
切片、映射和信道 本質(zhì)上為引用數(shù)據(jù)類型合敦,在使用前必須初始化初橘。 例如,切片是一個具有三項內(nèi)容的描述符充岛,包含一個指向(數(shù)組內(nèi)部)數(shù)據(jù)的指針保檐、長度以及容量, 在這三項被初始化之前崔梗,該切片為 nil夜只。
對于切片、映射和信道蒜魄,make 用于初始化其內(nèi)部的數(shù)據(jù)結(jié)構(gòu)并準(zhǔn)備好將要使用的值扔亥。
例如:
make([]int, 10, 100) 分配一個具有100個 int 的數(shù)組空間爪膊,接著創(chuàng)建一個長度為10, 容量為100并指向該數(shù)組中前10個元素的切片結(jié)構(gòu)
new([]int) 會返回一個指向新分配的砸王,已置零的切片結(jié)構(gòu)推盛, 即一個指向 nil 切片值的指針。
下面的例子闡明了 new 和 make 之間的區(qū)別:
var p *[]int = new([]int) // 分配切片結(jié)構(gòu)谦铃;*p == nil耘成;基本沒用
var v []int = make([]int, 100) // 切片 v 現(xiàn)在引用了一個具有 100 個 int 元素的新數(shù)組
// 沒必要的復(fù)雜:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// 習(xí)慣用法:
v := make([]int, 100)
再次說明關(guān)鍵點:
- make 只適用于映射、切片和信道且不返回指針驹闰。
- 若要獲得明確的指針瘪菌, 請使用 new 分配內(nèi)存。
數(shù)組
在規(guī)劃內(nèi)存布局時嘹朗,數(shù)組是非常有用的师妙,有時還能避免過多的內(nèi)存分配, 在Go中屹培,數(shù)組主要用作切片的構(gòu)件默穴,在構(gòu)建切片時使用。
數(shù)組在Go和C中的主要區(qū)別褪秀。在Go中:
- 數(shù)組是值蓄诽。將一個數(shù)組賦予另一個數(shù)組會復(fù)制其所有元素。
- 若將某個數(shù)組傳入某個函數(shù)媒吗,它將接收到該數(shù)組的一份副本而非指針仑氛。
- 數(shù)組的大小是其類型的一部分。類型 [10]int 和 [20]int 是不同的闸英。
數(shù)組為值的屬性很有用锯岖,但代價高昂;若你想要C那樣的行為和效率甫何,你可以傳遞一個指向該數(shù)組的指針出吹。
在 Go 中,更習(xí)慣的的用法是使用 切片沛豌。
切片
切片通過對數(shù)組進行封裝趋箩,為有序列的數(shù)據(jù)提供了更通用、強大而方便的方式加派。
除了矩陣變換這類需要明確維度的情況外,Go中的大部分數(shù)組編程都是通過切片來完成的跳芳。
切片保存了對底層數(shù)組的引用芍锦,若你將某個切片賦予另一個切片,它們會引用同一個數(shù)組飞盆。 若某個函數(shù)將一個切片作為參數(shù)傳入娄琉,則它對該切片元素的修改對調(diào)用者而言同樣可見次乓, 這可以理解為傳遞了底層數(shù)組的指針。
修改長度:只要切片不超出底層數(shù)組的限制孽水,它的長度就是可變的票腰,只需產(chǎn)生新的切片再次指向自身變量即可。
切片的長度:
len(切片)
切片的容量可通過內(nèi)建函數(shù) cap 獲得女气,它將給出該切片可取得的最大長度杏慰。函數(shù)為:
cap(切片)
若數(shù)據(jù)超出其容量,則會重新分配該切片炼鞠。返回值即為所得的切片缘滥。
向切片追加?xùn)|西的很常用,因此有專門的內(nèi)建函數(shù) 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 函數(shù)是預(yù)聲明的观游,且可用于任何切片類型肠仪。
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
for i, c := range data {
slice[l+i] = c
}
return slice
}
如上,輸入?yún)?shù)是切片和插入的元素值备典,返回值是切片异旧,注意切片的長度會發(fā)生變化。
因為盡管 Append 可修改 切片 的元素提佣,但切片自身(其運行時數(shù)據(jù)結(jié)構(gòu)包含指針吮蛹、長度和容量)是通過值傳遞的。
二維切片
要創(chuàng)建等價的二維數(shù)組或切片拌屏,就必須定義一個數(shù)組的數(shù)組潮针, 或切片的切片,示例:
type Transform [3][3]float64 // 一個 3x3 的數(shù)組倚喂,其實是包含多個數(shù)組的一個數(shù)組每篷。
type LinesOfText [][]byte // 包含多個字節(jié)切片的一個切片。
每行都有其自己的長度:
由于切片長度是可變的端圈,因此其內(nèi)部可能擁有多個不同長度的切片焦读。
映射 (map)
映射 是Go中 數(shù)據(jù)結(jié)構(gòu)中的 map結(jié)構(gòu)實現(xiàn),即 key: value的形式存儲舱权。
映射的值可以是各種類型矗晃。
映射的鍵可以是整數(shù)、浮點數(shù)宴倍、復(fù)數(shù)张症、字符串仓技、指針、接口等俗他。
映射的鍵(或者叫索引)可以是任何相等性操作符支持的類型脖捻, 如整數(shù)、浮點數(shù)兆衅、復(fù)數(shù)地沮、字符串、指針涯保、接口(只要其動態(tài)類型支持相等性判斷)诉濒、結(jié)構(gòu)以及數(shù)組。 切片不能用作映射鍵夕春,因為它們的相等性還未定義未荒。與切片一樣,映射也是引用類型及志。
如果將映射作為參數(shù)傳入函數(shù)中片排,并更改了該映射的內(nèi)容,則此修改對調(diào)用者同樣可見速侈。
映射可使用一般的復(fù)合字面語法進行構(gòu)建率寡,其鍵-值對使用逗號分隔,有點像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"]
注意:若試圖通過映射中不存在的鍵來取值倚搬,就會返回與該映射中項的類型對應(yīng)的零值冶共。例如,若某個映射包含整數(shù)每界,當(dāng)查找一個不存在的鍵時會返回 0捅僵。
判斷某個值是否存在:
seconds, ok = timeZone[tz]
上面是慣用的 "逗號 ok” 法:
- 若 tz 存在, seconds 就會被賦予適當(dāng)?shù)闹嫡2悖?ok 會被置為 true庙楚; - 若不存在,seconds 則會被置為零趴樱,而 ok 會被置為 false馒闷。
若僅需判斷映射中是否存在某項而不關(guān)心實際的值,可使用空白標(biāo)識符 _
來代替該值的一般變量叁征。
_, present := timeZone[tz]
要刪除映射中的某項纳账,可使用內(nèi)建函數(shù) delete
。即便對應(yīng)的鍵不在該映射中航揉,此操作也是安全的塞祈。
delete(timeZone, "PDT")
打印
Go的格式化打印風(fēng)格和C的 printf 類似,但卻更加豐富而通用帅涂。 這些函數(shù)位于 fmt 包中议薪,且函數(shù)名首字母均為大寫:如 fmt.Printf、fmt.Fprintf媳友,fmt.Sprintf 等斯议。
看例子:
// 以f 結(jié)尾的這幾個,傳入格式化字符串作為參數(shù), 不換行
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 用于構(gòu)造字符串: 字符串函數(shù)(Sprintf 等)會返回一個字符串,而不是寫入到數(shù)據(jù)流中焊唬。
Fprint 用于寫入到各種流中:fmt.Fprint 一類的格式化打印函數(shù)可接受任何實現(xiàn)了 io.Writer 接口的對象作為第一個實參恋昼;比如 os.Stdout 與 os.Stderr 。
下面對 Printf 支持的格式化的字符做一些說明:
-- 格式: %d
像 %d 不接受表示符號或大小的標(biāo)記赶促, 會根據(jù)實際的類型來決定這些屬性液肌。
var x uint64 = 1<<64 - 1 // x 是無符號整數(shù), 下面的 int64(x) 轉(zhuǎn)換為有符合整數(shù)
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
將打印
18446744073709551615 ffffffffffffffff; -1 -1
-- 格式: %v
%v 可理解為 實際的 value鸥滨。
它還能打印任意值嗦哆,甚至包括數(shù)組、結(jié)構(gòu)體和映射婿滓。
fmt.Printf("%v\n", timeZone) // 或只用 fmt.Println(timeZone)
這會輸出
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
%+v 和 %#v
當(dāng)打印結(jié)構(gòu)體時老速,格式 %+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
當(dāng)遇到 string 或 []byte 值時橘券, 可使用 %q 產(chǎn)生帶引號的字符串;而格式 %#q 會盡可能使用反引號卿吐。
--格式:%x
%x 還可用于字符串旁舰、字節(jié)數(shù)組以及整數(shù),并生成一個很長的十六進制字符串但两, 而帶空格的格式(% x)還會在字節(jié)之間插入空格鬓梅。
--格式: %T
它會打印某個值的類型.
fmt.Printf("%T\n", timeZone)
會打印
map[string] int
-- 為結(jié)構(gòu)圖自定義輸出
類似 java 中的 toString(),對結(jié)構(gòu)圖自定義類型的默認格式谨湘,只需為該類型定義一個具有 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"
-- 任意數(shù)量的
Printf 的簽名為其最后的實參使用了 ...interface{} 類型坊罢,這樣格式的后面就能出現(xiàn)任意數(shù)量,任意類型的形參了擅耽。
func Printf(format string, v ...interface{}) (n int, err error) {
在 Printf 函數(shù)的實現(xiàn)中活孩,v 看起來更像是 []interface{} 類型的變量,但如果將它傳遞到另一個變參函數(shù)中乖仇,它就像是常規(guī)實參列表了憾儒。實際上询兴,它直接將其實參傳遞給 fmt.Sprintln 進行實際的格式化。
// Println 通過 fmt.Println 的方式將日志打印到標(biāo)準(zhǔn)記錄器起趾。
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // Output 接受形參 (int, string)
}
注意上面的 ...interface{} 和 v... 的寫法诗舰。
追加 ( append 函數(shù) 說明 )
append 函數(shù)的簽名就像這樣:
func append(slice []T, 元素 ...T) []T
其中的 T 為任意給定類型的占位符。實際上训裆,你無法編寫一個類型 T 由調(diào)用者決定的函數(shù)眶根。這也就是為何 append 為內(nèi)建函數(shù)的原因:它需要編譯器的支持。
append 會在切片末尾追加元素并返回結(jié)果边琉。我們必須返回結(jié)果属百, 原因是,底層數(shù)組可能會被改變(注意數(shù)組的長度是類型的一部分)变姨。
以下簡單的例子
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
將打印
[1 2 3 4 5 6]
將一個切片追加到另一個切片很簡單:在調(diào)用的地方使用 ...
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
如果沒有 ...族扰,它就會由于類型錯誤而無法編譯,因為 y 不是 int 類型的钳恕。三個點符號 “ ...
” 的作用有點像“ 展開 ” 的作用别伏,即將 y這個切片的元素放到了這里。
初始化
GO 的huaGo的初始化很強大忧额,在初始化過程中厘肮,不僅可以構(gòu)建復(fù)雜的結(jié)構(gòu),還能正確處理不同包對象間的初始化順序睦番。
常量
常量在編譯時被創(chuàng)建类茂,即便函數(shù)中定義的局部變量也一樣。
常量只能是數(shù)字托嚣、字符(符文)巩检、字符串或布爾值。
由于編譯時的限制示启, 定義它們的表達式必須是可被編譯器求值的常量表達式兢哭。例如 1<<3 就是一個常量表達式。
枚舉常量
枚舉常量使用枚舉器 iota 創(chuàng)建夫嗓。由于 iota 可為表達式的一部分迟螺,而表達式可以被隱式地重復(fù),這樣也就更容易構(gòu)建復(fù)雜的值的集合了舍咖。
type ByteSize float64
const (
// 通過賦予空白標(biāo)識符來忽略第一個值
_ = 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 函數(shù)
每個源文件都可以通過定義自己的無參數(shù) init 函數(shù)來設(shè)置一些必要的狀態(tài)排霉。格式為:
func init() {
...
}
而 init 方法執(zhí)行結(jié)束窍株,就意味著初始化結(jié)束了:只有該包中的所有變量聲明都通過它們的初始化器求值后 init 才會被調(diào)用, 而那些 init 只有在所有已導(dǎo)入的包都被初始化后才會被求值。
init 函數(shù)還常被用在程序真正開始執(zhí)行前球订,檢驗或校正程序的狀態(tài)后裸。示例:
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath 可通過命令行中的 --gopath 標(biāo)記覆蓋掉。
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}
END