Golang笔记2--基础语法

作者: 奶爸撸代码 | 来源:发表于2019-08-12 00:57 被阅读4次

    Golang笔记2--基础语法

    一个大的程序是由很多小的基础构件组成的。变量保存值,简单的加法和减法运算被组合成较复杂的表达式。基础类型被聚合为数组或结构体等更复杂的数据结构。然后使用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 xxx
    • 包初始化解决包级变量的依赖顺序, 按声明顺序初始化, 多个文件按字母序发给编译器
    • 包初始化函数:func init(), 每个源文件可定义多个(建议1个), 不能被用户调用或引用
    • 在解决依赖情况下以导入声明的顺序初始化, main包最后初始化

    基础数据类型

    数字、字符串和布尔型。复合数据类型——数组结构体

    整型

    • 算术、逻辑和比较运算符(按优先级递减)
    *      /      %      <<       >>     &       &^
    +      -      |      ^
    ==     !=     <      <=       >      >=
    &&
    ||
    
    • 一元加减法(正负号)
    +      一元加法 (无效果)
    -      负数
    
    • 位操作符
    &      位运算 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迭代

    相关文章

      网友评论

        本文标题:Golang笔记2--基础语法

        本文链接:https://www.haomeiwen.com/subject/bsgejctx.html