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阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容