Golang筆記--基礎語法

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, breakcontinue,
// 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"

復數

提供兩種精度復數: complex64complex128

布爾型

不能直接和整型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}]"

基本的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-nilerror時,其他返回值是未定義的(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迭代

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容