Golang基礎語法
[TOC]
一個大的程序是由很多小的基礎構件組成的。變量保存值,簡單的加法和減法運算被組合成較復雜的表達式。基礎類型被聚合為數組或結構體等更復雜的數據結構。然后使用if和for之類的控制語句來組織和控制表達式的執行流程。然后多個語句被組織到一個個函數中,以便代碼的隔離和復用。函數以源文件和包的方式被組織
程序結構
命名
- Go命名規則:一個名字必須以一個字母(Unicode字母,所以中文也可)或下劃線開頭,后面可以跟任意數量的字母、數字或下劃線
- Keyword: 不能用于自定義名字
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
- 預定義的名字: 可重新定義
內建常量: true false iota nil
內建類型: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
內建函數: make len cap new append copy close delete
complex real imag
panic recover
- 頭字母的大小寫決定了名字在包外的可見性: 大寫開頭(函數外定義)可以被外部的包訪問
- Go語言風格是盡量使用短小的名字,尤其局部變量, 個人認為如影響理解則用具有意義的長命名
- Go語言程序員推薦使用駝峰式命名,縮寫全大寫
const lowerhex = "0123456789abcdef"
//QuoteRuneToASCII ...
func QuoteRuneToASCII(r rune) string
func appendQuotedRuneWith(buf []byte, r rune, quote byte, ASCIIonly, graphicOnly bool) []byte
- Go lint工具可幫助檢測命名是否合規
聲明
聲明語句定義了程序的各種實體對象以及部分或全部的屬性. Go語言主要有四種類型的聲明語句:var、const、type和func,分別對應變量、常量、類型和函數實體對象的聲明
- Go源文件以
go
作為后綴, 以包聲明開始 - 之后
import
導入依賴的包 - 包級的類型、變量、常量、函數的聲明,無順序(函數內必須先聲明)
package main
// 單行
// import "time"
// import log "github.com/sirupsen/logrus"
// 或者:
import(
"time"
// third-party包, 別名: log
log "github.com/sirupsen/logrus"
)
const version = "0.0.1"
// Printer is a exported struct
type Printer struct {
name string
}
//Print is a exported method
func(p *Printer) Print(){
_ = printTime(time.Now()) //nolint
}
// 函數
func printTime(t time.Time) error{
log.Info("now time: ", time.Now(), " version: ", version)
return nil
}
// 主函數
func main() {
var printer Printer
printer.Print()
}
-
函數的聲明
-
func
關鍵字 - 函數名字:
printTime
- 形參列表(變量名 變量類型, 可選, 由調用者提供實參):
t time.Time
- 返回值列表(可選, 多個需用括號):
error
- 函數體, 花括號內:
{...}
- struct方法還包含
receiver
:(p *Printer)
-
變量
-
var 變量名字 類型 = 表達式
, 未提供初始值則自動用零值初始化:var printer Printer
- 簡潔方式,冒號等號(無冒號則為賦值操作):
name := "tester"
,printer := Printer{}
- 多個變量:
var i,j,k int
var b, f, s = true, 2.3, "four" // bool, float64, string
var f, err = os.Open(name) //函數返回多個值
i, j := 0, 1
// 無冒號則為賦值操作
i, j = j, i // 交換 i 和 j 的值
指針變量
- 一個指針的值是另一個變量的地址,
- 通過指針,我們可以直接讀或更新對應變量的值
- 對于
var x int
聲明的變量x, 那么&x
(取x變量的內存地址)將產生一個指向x的指針 - 該指針對應的數據類型是
*int
- 指針零值都是
nil
- 返回函數中局部變量的地址也是安全的(自動垃圾回收機制)
- 指針示例:標準庫中flag包的關鍵技術,它使用命令行參數來設置對應變量的值
package main
import (
"flag"
"fmt"
"strings"
)
// flag.Bool函數會創建對應標志參數的變量:
// 三個屬性:名字“n”,默認值(這里是false),最后是描述信息
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()
}
}
- new內置函數
- 語法糖, 表達式new(T)將創建一個T類型的匿名變量
- 初始化為T類型的零值
- 返回變量地址,返回的指針類型為*T
變量生命周期和作用域
- 包變量貫穿整個程序運行周期(運行時)
- 部變量是動態的, 聲明到不再被引用
- 作用域是指源代碼中可以有效使用名字的范圍(編譯時概念)
- 句法塊是由花括弧所包含的一系列語句,塊內局部變量不能被塊外部訪問
- 內置類型(int,float等),內置函數, 常量等作用域全局,任何地方可用
- 控制流標號,就是break、continue或goto語句后標號,則是函數級的作用域
賦值
- 使用
=
號, 復合賦值:x *= scale
相當于x = x * scale
- 元組賦值:
x, y = y, x
交換x,y - 多個返回值賦值
f, err = os.Open("foo.txt")
- 這類函數會用額外的返回值來表達某種錯誤類型或bool判斷
- 用下劃線空白標識符_來丟棄不需要的值
// 后續了解
v, ok = m[key] // map lookup
v, ok = x.(T) // type assertion
v, ok = <-ch // channel receive
v = m[key] // map查找,失敗時返回零值
_, exists := m[key] // _占位
- 可賦值性
- 隱式賦值:
medals := []string{"gold", "silver", "bronze"}
- 只有右邊的值對于左邊的變量是可賦值的,賦值語句才是允許的
- 隱式賦值:
類型
type 類型名字 底層類型
- 命名類型還可以為該類型的值定義新的行為(方法集)
- 對于類型T, 都有一個對應的類型轉換操作T(x), 若T為指針可能還需要小括號:
(*int)(0)
- string []byte可轉換,數值可轉換
type Celsius float64 // 攝氏溫度
type Fahrenheit float64 // 華氏溫度
func(c Celsius) String() string{
return fmt.Sprintf("%g°C", c)
}
var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"
fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!
控制流結構
gpl并沒有專門章節講解基本控制流, 這里簡單列舉下吧.
- if條件語句, 跟c/c++比條件不需要括號
// condition 為真,否則
if condition {
...
} else {
...
}
// 慣用一
if ok:= function(); ok {
...
}
// 慣用二, 判定是否出錯
if val, err:= function(); err!=nil {
...
}
- switch語句
switch x := 0; x {
case 0:
fmt.Println(0)
case 1:
fallthrough
default:
fmt.Println("other")
}
// 不止是整型
switch coinflip() {
case "heads":
heads++
case "tails":
tails++
default:
fmt.Println("landed on edge!")
}
// 無tag, 表達式
func Signum(x int) int {
switch {
case x > 0:
return +1
default:
return 0
case x < 0:
return -1
}
}
- 循環語句, 不用小括號, 其他跟C/C++類似
i := 0
for ; i < 10; {
i++
}
// or
for i:=0; i<10; i++ {
}
// 無限循環
for {
}
//slice、數組的range迭代
for i,value:=range someSlice {
//...
}
//map range迭代
for k,v:=range someMap {
...
}
-
goto
,break
和continue
,
// goto語句可以無條件地轉移到過程中指定的行
if condition {
goto End
}
End:
close(xxx)
//跳出內層循環,不在執行循環
for {
if condition {
break
}
}
// continue 繼續下一次迭代
for {
if condition {
continue
}
// other states skipped
}
// 跳出外層循環, 使用Label
OutLoop:
for {
for i:=0; i<10;i++ {
if condition {
break OutLoop
}
}
}
-
select
多路復用,在select阻塞, 隨機選擇一個消息到達的case執行,詳細見后續并發章節
//for-select
Loop:
for {
select {
case v, ok:=<-someChan:
if !ok {
break Loop
}
//...
case time.After(time.Second):
break Loop
}
}
包和文件
- 包是為了支持模塊化、封裝、單獨編譯和代碼重用
- 每個包都對應一個獨立的名字空間, 引用時加包名:
fmt.Println
- 名字大寫字母開頭是從包中導出, 外部可調用
- 文件以
package xxx
開頭, xxx包 - 導入包:
import "fmt"
// or
import (
"io"
"time"
cql "github.com/sylladb/gocqlx" // alias
"golang.org/x/net/ipv4"
_ "net/http"
)
- 包初始化解決包級變量的依賴順序, 按聲明順序初始化, 多個文件按字母序發給編譯器
- 包初始化函數:
func init()
, 每個源文件可定義多個(建議1個), 不能被用戶調用或引用 - 在解決依賴情況下以導入聲明的順序初始化, main包最后初始化
- 命名盡量簡單,用單數(標準庫
errors
,bytes
,strings
,go/types
是為了避免與預定義類型或關鍵字沖突) - 不推薦直接使用util這種容易和變量沖突的包名, 如標準庫使用
imageutil
,ioutil
-
go list std | wc -l
查看標準包數目
基礎數據類型
數字、字符串和布爾型。復合數據類型——數組
和結構體
整型
- 算術、邏輯和比較運算符(按優先級遞減)
* / % << >> & &^
+ - | ^
== != < <= > >=
&&
||
- 一元加減法(正負號)
+ 一元加法 (無效果)
- 負數
- 位操作符
& 位運算 AND
| 位運算 OR
^ 位運算 二元操作符 XOR, 一元操作符為取反
&^ 位清空 (AND NOT)
<< 左移
>> 右移
浮點數
- math.MaxFloat32表示float32能表示的最大數值,大約是 3.4e38
- math.MaxFloat64常量大約是1.8e308
- %g, %f, %e(帶指數)
fmt.Printf("%8.3f\n", math.Exp(float64(x)))
- math.IsNaN()
- 正無窮大和負無窮大,分別用于表示太大溢出的數字和除零的結果;還有NaN非數,一般用于表示無效的除法操作結果0/0或Sqrt(-1)
var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"
復數
提供兩種精度復數: complex64
和complex128
布爾型
不能直接和整型0, 1轉換
字符串
- 一個字符串是一個不可改變的字節序列
- 文本字符串通常被解釋為采用UTF8編碼的Unicode碼點(rune)序列
- len函數可以返回字符串中的字節數目(不是rune字符數目, rune是int32等價類型)
- 利用UTF8編碼, UTF8是一個將Unicode碼點編碼為字節序列的變長編碼(1-4Bytes)
- 轉義,\uhhhh對應16bit的碼點值,\Uhhhhhhhh對應32bit
import "unicode/utf8"
w := "世界"
// "\xe4\xb8\x96\xe7\x95\x8c"
// "\u4e16\u754c"
// "\U00004e16\U0000754c"
s := "Hello, 世界"
fmt.Println(len(s)) // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%d\t%c\n", i, r)
i += size
}
fmt.Println(string(65)) // "A", not "65"
fmt.Println(string(0x4eac)) // "京"
標準庫中有四個包對字符串處理尤為重要:bytes、strings、strconv和unicode包
- 數字字符串轉換,
strconv
import "strconv"
x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"
x, err := strconv.Atoi("123") // x is an int
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits
- 字符串處理函數,
strings
:Contains
,Split
等
常量
const pi = 3.14159265358979323846264338327950288419716939937510582097494459
- 常量聲明可以使用
iota
常量生成器初始化
const (
_ = 1 << (10 * iota)
KiB // 1024
MiB // 1048576
GiB // 1073741824
TiB // 1099511627776 (exceeds 1 << 32)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (exceeds 1 << 64)
YiB // 1208925819614629174706176
)
復合數據類型
數組
- 元素個數明確指定, 可以用省略號(由初始化值個數決定)
var a [3]int
var a [...]int={1,2,3}
r := [...]int{99: -1} //100 items
- 實際示例: crypto/sha256包的Sum256函數對一個任意的字節slice類型的數據生成一個對應的消息摘要。消息摘要有256bit大小,因此對應[32]byte數組類型
切片slice
- slice(切片)代表變長的序列: []T,不指定元素個數
- 一個slice是一個輕量級的數據結構,提供了訪問數組子序列
- 切片操作
s[i:j]
,[3:]
,[:3]
,[:]
所有元素
- 切片操作
- 內置函數
make
, 創建一個匿名數組,返回一個slice -
cap
容量 - 內置函數
append
, 向slice追加元素, 可用于nil, 可追加多個元素,甚至追加一個slice - 兩slice不能直接比較相等, bytes.Equal函數判斷兩個字節型slice是否相等([]byte)
make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
// append
var s,ss []string
s = append(s, 'a')
ss = append(ss, s...)
- 類似于:
type IntSlice struct {
ptr *int
len, cap int
}
map
哈希表是一個無序的key/value對的集合,key唯一,通過給定的key可以在常數時間復雜度內檢索、更新或刪除對應的value, map類型的零值是nil
// 創建,`make`可創建map
ages1 := make(map[string]int)
ages1["alice"] = 32
ages2 := map[string]int{
"alice": 31,
"charlie": 34,
}
// 刪除對應key元素
delete(ages, "alice")
// 是否存在
if _, exists:= ages2["alice"]; exists{
fmt.Println("exists")
}
// access
ages["bob"]++
// range遍歷
for k, v:=range ages2{
fmt.Println(k,v)
}
- map中的元素并不是一個變量,因此我們不能對map的元素進行取址操作(因為地址會變)
- 不能直接相等, 要判斷兩個map是否包含相同的key和value,要通過循環實現
- 類似集合可以使用
map[T]bool
實現 - map和slice參數傳引用
結構體
- 結構體由零個或多個任意類型的值聚合而成, 每個值稱為結構體的成員
- 成員的輸入順序有意義(如以下
Name
,Address
不同順序則為不同結構體) - 考慮效率的話,較大的結構體通常會用指針的方式傳入和返回
- 如果所有成員可比較, 則結構體可以比較(==, !=),且可用作map的key
- 結構體類型的零值是每個成員都是零值(第9章sync.Mutex零值為未鎖定狀態)
// 一般一行對應一個成員,也可以合并, 成員名字在前,類型在后
type Employee struct {
ID int
Name,Address string
}
var dilbert Employee
- S類型的結構體可以包含*S指針類型的成員, 如以下二叉樹實現插入排序
type tree struct {
value int
left, right *tree
}
// Sort sorts values in place.
func Sort(values []int) {
var root *tree
for _, v := range values {
root = add(root, v)
}
appendValues(values[:0], root)
}
// appendValues appends the elements of t to values in order
// and returns the resulting slice.
func appendValues(values []int, t *tree) []int {
if t != nil {
values = appendValues(values, t.left)
values = append(values, t.value)
values = appendValues(values, t.right)
}
return values
}
func add(t *tree, value int) *tree {
if t == nil {
// Equivalent to return &tree{value: value}.
t = new(tree)
t.value = value
return t
}
if value < t.value {
t.left = add(t.left, value)
} else {
t.right = add(t.right, value)
}
return t
}
- 結構體字面值, 注意: 以下兩種方式不能混用, 且不能對未導出成員使用
type Point struct{ X, Y int }
// 按照順序
p := Point{1, 2}
// 指定成員名字
anim := gif.GIF{LoopCount: nframes}
- 較大的結構體通常會用指針的方式傳入和返回
- 如果要在函數內部修改結構體成員的話,必須指針傳入, 因為Go函數傳值調用
pp := &Point{1, 2}
// 等價于
pp := new(Point)
*pp = Point{1, 2}
- 結構體嵌入和匿名成員, 可簡化編程, 簡單實現繼承, 看以下圓和輪的演進:
// 原始版本
type Circle struct {
X, Y, Radius int
}
type Wheel struct {
X, Y, Radius, Spokes int
}
相同屬性獨立出來, 便于維護
type Point struct {
X, Y int
}
type Circle struct {
Center Point
Radius int
}
type Wheel struct {
Circle Circle
Spokes int
}
但是訪問繁瑣:
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
結構體內只聲明數據類型而不指名成員名,這類成員就叫匿名成員
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
這樣訪問成員(顯式形式訪問這些內部成員的語法依然有效):
var w Wheel
// 快捷方式
w.X = 8 // equivalent to w.Circle.Point.X = 8
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 20
但字面值定義需要遵循層次:
w = Wheel{Circle{Point{8, 8}, 5}, 20} //or
w = Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}
fmt.Printf("%#v\n", w)
注意:
- fmt的
%#v
將打印成員名,不止于值 - 因為有隱式的名字, 不能同時包含兩個類型相同的匿名成員
- 包外使用時, 未導出成員無法用簡化方式訪問
json
標準庫中的encoding/json、encoding/xml、encoding/asn1等包提供支持, 另外還有大量第三方json庫可用(protobuf的jsonpb,jsoniter...)
- 基本類型有數字(int, float),布爾型(true,false), 字符串(雙引號包含的unicode字符序列)
- 復合類型, 數組(可編碼Golang的數組和slice), 對象(可編碼Golang的map和結構體)
- struct定義中成員之后反引號的tag可定義json
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actors []string
}
struct轉換為json的過程叫編碼(marshaling):
data, err := json.Marshal(movies)
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)
輸出無縮進,難以閱讀(注意: 在最后一個成員或元素后面并沒有逗號分隔符):
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr
id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac
tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"
Actors":["Steve McQueen","Jacqueline Bisset"]}]
因此,還可以使用:
data, err := json.MarshalIndent(movies, "", " ")
//...
- 編碼的逆操作是解碼,對應將JSON數據解碼為Go語言的數據結構(unmarshaling)
var titles []struct{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
- GitHub的Web服務接口
- 摘錄譯文版兩段說明:
基本的JSON類型有數字(十進制或科學記數法)、布爾值(true或false)、字符串,其中字符串是以雙引號包含的Unicode字符序列,支持和Go語言類似的反斜杠轉義特性,不過JSON使用的是\Uhhhh轉義數字來表示一個UTF-16編碼(譯注:UTF-16和UTF-8一樣是一種變長的編碼,有些Unicode碼點較大的字符需要用4個字節表示;而且UTF-16還有大端和小端的問題),而不是Go語言的rune類型。
這些基礎類型可以通過JSON的數組和對象類型進行遞歸組合。一個JSON數組是一個有序的值序列,寫在一個方括號中并以逗號分隔;一個JSON數組可以用于編碼Go語言的數組和slice。一個JSON對象是一個字符串到值的映射,寫成以系列的name:value對形式,用花括號包含并以逗號分隔;JSON的對象類型可以用于編碼Go語言的map類型(key類型是字符串)和結構體
文本和HTML模板
text/template和html/template提供模板相關支持
一個模板是一個字符串或一個文件,里面包含了一個或多個由雙花括號包含的{{action}}對象
const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User: {{.User.Login}}
Title: {{.Title | printf "%.64s"}}
Age: {{.CreatedAt | daysAgo}} days
{{end}}`
func daysAgo(t time.Time) int {
return int(time.Since(t).Hours() / 24)
}
- action中
|
操作符表示將前一個表達式的結果作為后一個函數的輸入,類似于UNIX中管道 - 生成模板的輸出的處理步驟:
- 第一步是要分析模板(執行一次即可)并轉為內部表示
- 然后基于指定的輸入執行模板
// 調用鏈順序:
// template.New先創建并返回一個模板;
// uncs方法將daysAgo等自定義函數注冊到模板中,并返回模板;
// 最后調用Parse函數分析模板
report, err := template.New("report").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ)
if err != nil {
log.Fatal(err)
}
- 模板解析失敗是致命錯誤(編譯前測試好), template.Must輔助函數可以簡化處理
// 模板解析失敗是致命錯誤(編譯前測試好), template.Must輔助函數可以簡化處理
var report = template.Must(template.New("issuelist").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ))
func main() {
result, err := github.SearchIssues(os.Args[1:])
if err != nil {
log.Fatal(err)
}
if err := report.Execute(os.Stdout, result); err != nil {
log.Fatal(err)
}
}
-
html/template
模板包類似, 但是增加了字符串自動轉義特性- 避免輸入字符串和HTML、JavaScript、CSS或URL語法產生沖突的問題
- 避免一些安全問題,諸如HTML注入攻擊
import "html/template"
var issueList = template.Must(template.New("issuelist").Parse(`
<h1>{{.TotalCount}} issues</h1>
<table>
<tr style='text-align: left'>
<th>#</th>
<th>State</th>
<th>User</th>
<th>Title</th>
</tr>
{{range .Items}}
<tr>
<td><a href='{{.HTMLURL}}'>{{.Number}}</a></td>
<td>{{.State}}</td>
<td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
<td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
</tr>
{{end}}
</table>
`))
函數
聲明(見前)
- 函數聲明包括函數名、形參列表、返回值列表(可省略)以及函數體
func name(parameter-list) (result-list) {
body
}
- 函數的類型被稱為函數的標識符, 形參和返回值類型一一對應被認為有相同的類型和標識符
type HandleFunc func(http.ResponseWriter, *http.RequestReader)
- 函數可遞歸,即可直接或間接地調用自身
- Golang函數可多值返回, 小括號包含
- 返回值可指定變量名, 相同類型指定有意義的命名可增加可讀性
// width, height
func Size(rect image.Rectangle) (width, height int)
錯誤
- 函數返回一個額外的返回值,通常是最后一個,來傳遞錯誤信息(
error
)。如果導致失敗的原因只有一個,額外的返回值可以是一個布爾值(bool
) - 通常,當函數返回
non-nil
的error
時,其他返回值是未定義的(undefined
),應該被忽略 - 某些情況其他值可返回有意義值, 如文件讀寫失敗, 仍然會返回讀寫字節數, 這種情況應該是先處理不完整的數據,再處理錯誤
- EOF錯誤, 由文件讀取結束引發的讀取失敗
關于不使用異常的說明:
Go這樣設計的原因是由于對于某個應該在控制流程中處理的錯誤而言,將這個錯誤以異常的形式拋出會混亂對錯誤的描述,這通常會導致一些糟糕的后果
錯誤處理策略
- 向上傳播
- 描述詳盡, 包含上下文
- 錯誤信息經常是以鏈式組合在一起的,所以錯誤信息中應避免
大寫
和換行符
- 重試策略
- 偶然性的
- 或由不可預知的問題導致
- 輸出錯誤信息并結束程序
- main函數
- 程序內部包含不一致,即bug導致
- log.Fatal
- 僅打印信息,不中斷不重試
- 直接忽略策略
函數值
被看作第一類值(first-class values):函數像其他值一樣,擁有類型,可以被賦值給其他變量,傳遞給函數,從函數返回
- 函數類型的零值是nil, 可與nil比較,nil調用會
panic
- 函數值之間不可比較, 不能用函數值作為map的key
- 匿名函數: func關鍵字后沒有函數名, 繞過函數只能在包級別定義的限制
// 匿名函數
add1:= func(r rune) rune { return r + 1 }
fmt.Println(strings.Map(add1, "VMS")) // "WNT"
fmt.Println(strings.Map(func(r rune)rune {
return r + 1
}, "VMS")
警告:匿名函數捕獲迭代變量
循環迭代中,函數值中記錄的迭代變量(作用域在for詞法塊,在該循環中生成的所有函數值都共享相同的循環變量)地址而不是值
注意以下賦值: dir := d
var rmdirs []func()
for _, d := range tempDirs() {
dir := d // NOTE: necessary!
os.MkdirAll(dir, 0755) // creates parent directories too
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}
// ...do some work…
for _, rmdir := range rmdirs {
rmdir() // clean up
}
后續遇到defer語句或for循環中goroutine(go func(){...}
)類似!!!
可變參數
unc sum(vals...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
后續會遇到可變option個數傳遞
deferred函數
defer someFuncion()
- 在包含該defer語句的函數其他語句完畢后才執行
- 多個defer后來先執行
- defer語句經常被用于處理成對的操作,如打開、關閉、連接、斷開連接、加鎖、釋放鎖
- 通過defer機制保證在任何執行路徑下,資源被釋放
- 釋放資源的defer應直接跟在請求資源的語句后
func title(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
return fmt.Errorf("%s has type %s, not text/html",url, ct)
}
doc, err := html.Parse(resp.Body)
if err != nil {
return fmt.Errorf("parsing %s as HTML: %v", url,err)
}
// ...print doc's title element…
return nil
panic和recover
當panic異常發生時,程序會中斷運行,并立即執行在該goroutine(可以先理解成線程,在第8章會詳細介紹)中被延遲的函數(defer 機制)。隨后,程序崩潰并輸出日志信息
- 直接調用內置的panic函數也會引發panic異常, 到達邏輯上不可達的路徑可以panic
- panic會引起程序的崩潰,因此一般用于嚴重錯誤,如程序內部的邏輯不一致
- 明確正則表達式(大多數是字符串字面值)不會出錯,可使用
regexp.MustCompile
檢查輸入
通常來說,不應該對panic異常做任何處理,但有時,也許我們可以從異常中恢復,至少我們可以在程序崩潰前,做一些操作。舉個例子,當web服務器遇到不可預料的嚴重問題時,在崩潰前應該將所有的連接關閉;如果不做任何處理,會使得客戶端一直處于等待狀態
如果在deferred函數中調用了內置函數recover,并且定義該defer語句的函數發生了panic異常,recover會使程序從panic中恢復,并返回panic value。導致panic異常的函數不會繼續運行,但能正常返回。在未發生panic時調用recover,recover會返回nil。
deferred函數幫助Parse從panic中恢復。在deferred函數內部,panic value被附加到錯誤信息中;并用err變量接收錯誤信息,返回給調用者。我們也可以通過調用runtime.Stack往錯誤信息中添加完整的堆棧調用信息。
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}
注意: 不應該試圖去恢復其他包引起的panic(有時難以做到),安全的做法是有選擇性的recover
方法
- 方法是一個和特殊類型關聯的函數, 面向對象編程概念.
- 方法關聯一個被稱為接收器的對象
- 指針對象可避免復制, 可修改成員變量, 否則修改復制對象的成員,改不了原來的對象
- 不管receiver是指針類型還是非指針類型,都可以通過指針/非指針類型進行調用的,編譯器會根據方法自動轉換
- Nil也是一個合法的接收器類型
通過嵌套struct繼承方法
- 嵌入的struct方法可以被重新定義, 外部結構在其方法可以顯式調用嵌入對象的方法
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
示例: sync.Mutex的Lock和Unlock方法被引入到匿名結構中:
var cache = struct {
sync.Mutex
mapping map[string]string
}{
mapping: make(map[string]string),
}
func Lookup(key string) string {
cache.Lock()
v := cache.mapping[key]
cache.Unlock()
return v
}
方法值和方法表達式
distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
bitmap
通常使用map[T]bool來表示集合, 但是用bitmap(byte[]實現)是種更好的選擇:
- 例如在數據流分析領域, 集合通常是非負整數
- http分塊下載文件(16KB每塊),可用bimap標記下載完成的塊
// An IntSet is a set of small non-negative integers.
// Its zero value represents the empty set.
type IntSet struct {
words []uint64
}
// Has reports whether the set contains the non-negative value x.
func (s *IntSet) Has(x int) bool {
word, bit := x/64, uint(x%64)
return word < len(s.words) && s.words[word]&(1<<bit) != 0
}
// Add adds the non-negative value x to the set.
func (s *IntSet) Add(x int) {
word, bit := x/64, uint(x%64)
for word >= len(s.words) {
s.words = append(s.words, 0)
}
s.words[word] |= 1 << bit
}
// UnionWith sets s to the union of s and t.
func (s *IntSet) UnionWith(t *IntSet) {
for i, tword := range t.words {
if i < len(s.words) {
s.words[i] |= tword
} else {
s.words = append(s.words, tword)
}
}
}
// String returns the set as a string of the form "{1 2 3}".
func (s *IntSet) String() string {
var buf bytes.Buffer
buf.WriteByte('{')
for i, word := range s.words {
if word == 0 {
continue
}
for j := 0; j < 64; j++ {
if word&(1<<uint(j)) != 0 {
if buf.Len() > len("{") {
buf.WriteByte(' ')
}
fmt.Fprintf(&buf, "%d", 64*i+j)
}
}
}
buf.WriteByte('}')
return buf.String()
}
注意: bytes.Buffer的String()用法, 定義Strin()有助于fmt.Print會調用打印, 這種機制有賴于接口和類型斷言(詳見下一章)
封裝
OOB編程很重要的一點就是封裝(信息隱藏), 三個好處:
- 最少知識: 無需調用方了解所有細節, 僅需少量接口即可
- 依賴抽象: 隱藏實現的細節,可以防止調用方依賴那些可能變化的具體實現
- 防止外部調用方對對象內部的值任意地進行修改
回顧上一節的IntSet定義:
type IntSet struct {
words []uint64
}
其實也可以這樣定義:
type IntSet []uint64
但是后者封裝性不如前者, 因為words
成員是包外不可見的, 無法直接操作
封裝并不總是需要的, 比如time
包的Duration
暴露為int64的納秒, 這樣自定義相關常量成為可能:
const day = 24 * time.Hour
另外如第二種方式暴露內部slice成員, 就可以直接用range迭代