美文网首页
工作中用Go: Go基础

工作中用Go: Go基础

作者: daydaygo | 来源:发表于2023-01-13 12:11 被阅读0次

    背景介绍

    工作中用Go: 工具篇 - 简书 (jianshu.com) 介绍了相关工具的使用, 这篇聚集 Go基础.

    Go is simple but not easy.

    Go 很简单,但不容易掌握

    type: 类型系统

    先说结论:

    • 用法: 类型声明 declare; 类型转换 trans; 类型别名 alias; 类型断言 assert
    • 值类型 vs 指针类型
    • 0值可用

    Go内置关键字, 大部分可以直接从源码中查看 <Go>/src/builtin/builtin.go 中查看, 其中大部分都是Go内置类型

    怎么快速查看Go源码, 可以访问上一篇blog: 工作中用Go: 工具篇 - 简书 (jianshu.com)

    var 变量

    变量的本质: 特定名字 <-> 特定内存块

    • 静态语言: 变量所绑定的********内存********区域是要有一个明确的边界的 -> 知道类型才能知道大小
      • 指针: 指针虽然大小固定(32bit/64bit, 依赖平台), 但是其指向的内存, 必须知道类型, 才能知道大小

    变量声明的3种方式:

    • := 推荐, 支持类型自动推导, 常用分支控制中的局部变量
    • var
    • 函数的命名返回值
    // 申明且显式初始化
    a := int(10) 
    
    // 默认为0值
    var a int
    
    func A() (a int) // 命名返回值相当于 var a int
    
    

    变量的作用域(scope)

    • 包级别变量: 大写可导出
    • 局部变量: 代码库(block{}) 控制语句(for if switch)

    变量常见问题 - 变量遮蔽(variable shadowing): 定义了同名变量, 容易导致变量混淆, 产生隐藏bug且难以定位

    a, err := A()
    // do something
    b, err := B() // 再次定义同名 err 变量
    
    

    type alias 类型别名

    类型别名(type alias)的存在,是 渐进式代码修复(Gradual code repair) 的关键

    // <Go>/src/builtin/builtin.go
    
    // rune is an alias for int32 and is equivalent to int32 in all ways. It is
    // used, by convention, to distinguish character values from integer values.
    type rune = int32
    
    

    类型别名其实是对现实世界的一种映射, 同一个事物拥有不同的名字的场景太多, 比如 apple苹果, 再比如 土豆马铃薯 , 更有意思的一个例子:

    你们抓周树人,关我鲁迅什么事? -- 《楼外楼》

    0值

    Go中基础类型和0值对照表:

    type 0值
    int byte rune 0
    float 0.0
    bool false
    string ""
    struct 字段都为0值
    slice map pointer interface func nil

    关于 nil, 可以从源码中获取到详细信息:

    // <Go>/src/builtin/builtin.go
    
    // nil is a predeclared identifier representing the zero value for a
    // pointer, channel, func, interface, map, or slice type.
    var nil Type // Type must be a pointer, channel, func, interface, map, or slice type
    
    

    func 也只是类型的一种:

    t := T{}
    f := func(){} // 函数字面值.FunctionLiteral
    
    type HandlerFunc func(ResponseWriter, *Request)
    http.HandlerFunc(hello) // hello 和 HandlerFunc 出入参相同, 所以才能进行类型转换
    func hello(writer http.ResponseWriter, request *http.Request) {
     // fmt.Fprintln(writer, "<h1>hello world</h1>")
     fmt.Fprintf(writer, "<h1>hello world %v</h1>", request.FormValue("name"))
    }
    
    

    值类型 vs 指针类型

    结合上面变量的本质来理解:

    变量的本质: 特定名字 <-> 特定内存块

    那么值类型和指针类型就很容易理解: 值类型在函数调用过程中会发生复制, 指向新的内存块, 而指针则指向同一块内存

    再结合上面的0值, 有一个简单的规则:

    0值的为 nil 的类型, 函数调用时不会发生复制

    当然, 这条规则还需要打上不少补丁, 我们在后面继续聊

    还有一个经典问题: 值类型 vs 指针类型, 怎么选 / 用哪个?

    其实回答这个问题, 只需要列举几个 Must 的 case 即可:

    • noCopy: 不应该复制的场景, 这种情况必须使用指针类型, 尤其要注意 struct, 默认是值类型T, 如果有 noCopy 字段, 必须使用指针类型*T
    // 源码中 sync.Mutex 上的说明
    // A Mutex must not be copied after first use.
    
    // Go中还有特殊 noCopy 类型
    // noCopy may be added to structs which must not be copied
    // after the first use.
    //
    // See https://golang.org/issues/8005#issuecomment-190753527
    // for details.
    //
    // Note that it must not be embedded, due to the Lock and Unlock methods.
    type noCopy struct{}
    
    // Lock is a no-op used by -copylocks checker from `go vet`.
    func (*noCopy) Lock()   {}
    func (*noCopy) Unlock() {}
    
    
    • 不应当复制的场景: 比如结构体使用 []byte 字段, 如果使用值类型T 导致 []byte 在调用过程中产生复制, 会大大影响性能, 这种情况就要使用*T, 更多细节, 可以参考这个地址: 03 Decisions | Google Style Guides (gocn.github.io)
    // Good:
    type Record struct {
      buf bytes.Buffer
      // other fields omitted
    }
    
    func New() *Record {...}
    
    func (r *Record) Process(...) {...}
    
    func Consumer(r *Record) {...}
    
    // Bad:
    type Record struct {
      buf bytes.Buffer
      // other fields omitted
    }
    
    func (r Record) Process(...) {...} // Makes a copy of r.buf
    
    func Consumer(r Record) {...} // Makes a copy of r.buf
    
    

    0值可用

    大部分情况下, Go中的类型都是满足 0值可用 的, 需要注意几个点:

    • map 不是 0值可用 , 必须进行初始化
      • 使用 make, 如果知道大小也可以预先指定
      • 初始化对应值
      • 函数命名返回值中的map (m map[int]int, 需要显式初始化一次
    m := make(map[int]int, 10) // 推荐
    
    var m = map[int]int{1:1} // 初始化对应值
    
    
    • 0值可用的特殊类型: sync.Mutex sync.Once ...
    // 以 sync.Mutex 的使用举例
    var mu sync.Mutex // 零值不需要额外初始化
    
    type Counter struct {
     Type int
     Name string
    
     mu  sync.Mutex // 1.放在要控制的字段上面并空行 2.内嵌字段
     cnt uint64
    }
    
    // 1.封装成方法 
    // 2.读写都需要
    func (c *Counter) Incr() {
     c.mu.Lock()
     c.cnt++
     c.mu.Unlock()
    }
    func (c *Counter) Cnt() uint64 {
     c.mu.Lock()
     defer c.mu.Unlock()
     return c.cnt
    }
    
    
    • 在具体实践过程中, 类型在申明时没有赋值会自动赋0值, 就需要注意0值什么满足业务需求, 比如:
    type ReqListReq struct {
     Month     string     `form:"month"`      // 期间, 格式: 202212
     Status    pay.Status `form:"status"`     // 审批状态
    }
    
    type Status int // 审批状态
    
    const (
     StatusNone    Status = iota // 0值
     StatusInit                  // 未开始
     StatusIng                   // 审批中
     StatusDone                  // 已通过
     StatusReject                // 已拒绝
     StatusCancel                // 已撤回
    )
    
    

    如果请求带了 status 查询条件, 则一定非0值

    语法和易错点

    byte / rune / string

    type rune = int32: Go中用 rune 表示一个 utf8 编码, 在 utf8 中, 一个字符由 1-4字节 来编码

    len("汉") // 3
    utf8.RuneCountInString("汉") // 1
    
    []byte("汉") // []byte{0xE6, 0xB1, 0x89}
    []rune("汉")
    
    

    遍历string:

    • for-i / s[0] -> byte
    • for-range -> rune

    字符串拼接:

    • + +=
    • fmt
    • strings 或者 bytes 包中的方法: strings.Builder

    性能上的注意点:

    • string 和 []byte 的转换会分配内存, 一个推荐的做法是 []byte 使用 bytes 包中的方法, 基本 strings 包有的功能, bytes 包都有
    • 使用 strings.Builder 时一定要使用 Grow(), 底层是使用的 slice

    slice

    • 预先指定大小, 减少内存分配
      • len 需要指定为 0, len不为0时会将 len 个元素全部设为0值, appendlen 后的元素开始
    s := make([]int, 0, 10)
    s = append(s, 10)
    
    
    • 切片的切片: slice 底层使用的 array , 由于 slice 会自动扩容, 在使用切片的切片时, 就一定要小心: 发生写操作时, 是否会影响到原来的切片?

    map

    • map不是0值可用, 上面👆🏻已经讲到

    • map是无序的, 而且是开发组特意加的, 原因可以参考官方blog, 这一条说起来简单, 但是实践上却非常容易犯错, 特别是使用 map 返回 keys / values 集合的情况

      • 查询使用的下拉框
      • 查询多行数据后使用 map 拼接数据, 然后使用map返回 values
    • 解决map无序通常2个方法

      • 使用slice保证顺序: 比如上面的例子, 申明了个 slice 就好了, 因为元素都是指针, 让map去拼数据, 后续返回的 slice 就是最终结果了
      • 使用sort.Slice 排序
    • map无序还会影响一个骚操作: for-range 遍历map的时候新增key, 新增的key不一定会被遍历到

    sort.Slice(resp, func(i, j int) bool {
     return resp[i].MonthNumber < resp[j].MonthNumber
    })
    
    
    • map没有使用 ok 进行判断, 尤其是 map[k]*T 的场景, 极易导致 runtime error: invalid memory address or nil pointer dereference
    • map不是并发安全, 真这样写了, 编译也不会通过的😅
    • map实现set, 推荐使用 struct{} 明确表示不需要value
    type Set[K comparable] map[K]struct{}
    

    struct

    • 最重要的其实上面已经介绍过的: T是值类型, *T是指针类型
      • T在初始化时默认会把所有字段设为为0值
      • *T 默认是nil, 其实是不可用状态, 必须要初始化后才能使用
      • 值类型T会产生复制, 要注意 noCopy 的场景
    var t T // T的所有字段设置为0值进行初始化
    
    var t *T // nil, 不推荐, t必须初始化才能使用
    (t *T) // 函数的命名返回值也会踩这个坑
    
    t := &T{} // 等价的, 都是使用 0 值来初始化T并返回指针, 推荐使用 &T{}
    t := new(T)
    
    
    • 还有2个奇淫巧技
    1. struct{} 是 0 内存占用, 可以在一些优化一些场景, 不需要分配内存, 比如
    • 上面的 type Set[K comparable] map[K]struct{}
    • chan: chan struct{}
    1. struct 内存对齐(aligned)
    // 查看内存占用: unsafe.Sizeof
    i := int32(10)
    s := struct {}{}
    fmt.Println(unsafe.Sizeof(i)) // 4
    fmt.Println(unsafe.Sizeof(s)) // 0
    
    // 查看内存对齐后的内存占用
    unsafe.Alignof()
    
    

    for

    • for-range 循环, 循环的 v 始终指向同一个内存, 每次都讲遍历的元素, 进行值复制给 v
    // bad
    var a []T
    var b []*T
    for _, v := range a {
     b = append(b, &v) // &V 都是指向同一个地址, 最后导致 b 中都是相同的元素
    }
    
    
    • for+go 外部变量 vs 传参
    // bad
    for i := 0; i < 10; i++ {
     go func() {
      println(i) // 协程被调度时, i 的值并不确定
     }()
    }
    
    // good
    for i := 0; i < 10; i++ {
     go func(i int) {
      println(i)
     }(i)
    }
    
    

    break

    break + for/select/switch 只能跳出一层循环, 如果要跳出多层循环, 使用 break label

    switch

    Go中的switch和以前的语言有很大的不同, break只能退出当前switch, 而 Go 中 switch 执行完当前 case 就会退出, 所以大部分情况下, break 都可以省略

    func

    • 出入参: 还是上面的内容, 值类型 vs 指针类型, 需要注意的是: string/slice/map 作为入参, 只是传了一个描述符进来, 并不会发生全部数据的拷贝
    • 变长参数: func(a ...int) 相当于 a []int
    • 具名返回值(a int) 相当于 var a int, 考虑到 0值可用, 一定要注意是否要对变量进行初始化
      • 适用场景: 相同类型的值进行区分, 比如返回经纬度; 简短函数简化0值申明, 是函数更简洁
    • func也是一种类型, var f func()func f() 函数签名相同(出入参相同)时可以进行类型转换

    err

    • 惯例
      • 如果有 err, 作为函数最后一个返回值
      • 预期内err, 使用 value; 非预期err, 使用 type
    // 初始化
    err := errors.New("xxx")
    err := fmt.Errorf("%v", xxx)
    
    // wrap
    err := fmt.Errorf("wrap err: %w", err)
    
    // 预期内err
    var ErrFoo = errors.New("foo")
    
    // 非预期err, 比如 net.Error
    // An Error represents a network error.
    type Error interface {
     error
     Timeout() bool // Is the error a timeout?
    
     // Deprecated: Temporary errors are not well-defined.
     // Most "temporary" errors are timeouts, and the few exceptions are surprising.
     // Do not use this method.
     Temporary() bool
    }
    
    
    • 推荐使用 pkg/errors, 使用 %v 可以查看err信息, 使用 %+v 可以查看调用栈
      • 原理是实现了 type Formatter interface
    // Format formats the frame according to the fmt.Formatter interface.
    //
    //    %s    source file
    //    %d    source line
    //    %n    function name
    //    %v    equivalent to %s:%d
    //
    // Format accepts flags that alter the printing of some verbs, as follows:
    //
    //    %+s   function name and path of source file relative to the compile time
    //          GOPATH separated by \n\t (<funcname>\n\t<path>)
    //    %+v   equivalent to %+s:%d
    func (f Frame) Format(s fmt.State, verb rune) {
     switch verb {
     case 's':
      switch {
      case s.Flag('+'):
       io.WriteString(s, f.name())
       io.WriteString(s, "\n\t")
       io.WriteString(s, f.file())
      default:
       io.WriteString(s, path.Base(f.file()))
      }
     case 'd':
      io.WriteString(s, strconv.Itoa(f.line()))
     case 'n':
      io.WriteString(s, funcname(f.name()))
     case 'v':
      f.Format(s, 's')
      io.WriteString(s, ":")
      f.Format(s, 'd')
     }
    }
    
    

    defer

    • 性能: Go1.17 优化过, 性能损失<5% -> 放心使用
    • 场景
      • 关闭资源: defer conn.Close()
      • 配套使用的函数: defer mu.Unlock()
      • recover()
      • 上面场景以外的骚操作, 需谨慎编码

    panic

    运行时 / panic() 产生, panic 会一直出栈, 直到程序退出或者 recover, 而 defer 一定会在函数运行后执行, 所以:

    • recover() 必须放在 defer 中执行, 保证能捕捉到 panic
    • 当前协程的 panic 只能被当前协程的 recover 捕获, 一定要小心 野生goroutine, 详细参考这篇blog:

    Go源码中还有一种用法: 提示潜在bug

    // json/encode.go resolve()
    func (w *reflectWithString) resolve() error {
     if w.k.Kind() == reflect.String {
      w.ks = w.k.String()
      return nil
     }
     if tm, ok := w.k.Interface().(encoding.TextMarshaler); ok {
      if w.k.Kind() == reflect.Pointer && w.k.IsNil() {
       return nil
      }
      buf, err := tm.MarshalText()
      w.ks = string(buf)
      return err
     }
     switch w.k.Kind() {
     case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
      w.ks = strconv.FormatInt(w.k.Int(), 10)
      return nil
     case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
      w.ks = strconv.FormatUint(w.k.Uint(), 10)
      return nil
     }
     panic("unexpected map key type") // 正常情况不会走到这里, 如果走到了, 就有潜在bug
    }
    
    

    方法(method)

    • 方法的本质, 是将 receiver 作为函数的第一个参数: func (t *T) xxx(){} -> func xxx(t *T, ){}
    • Go有一个语法糖, 无论使用 T 还是 *T 的方法, 都可以调用, 但是需要注意细微的差别:
      • 最重要的: 值类型 vs 指针类型, 尤其是只能使用 *T 的场景
      • 实现 interface 时, *T 可以使用所有方法, 而 T 只能使用 T 定义的方法

    interface

    • 最重要的一点: interface = type + value, 下面有个很好的例子
    func TestInterface(t *testing.T) {
     var a any
     var b error
     var c *error
     d := &b
     t.Log(a == b)   // true
     t.Log(a == c)   // false
     t.Log(c == nil) // true
     t.Log(d == nil) // false
    }
    
    

    使用 debug 查看:

    debug: interface & nil
    • 尽量使用小接口(1-3个方法): 抽象程度更高 + 易于实现和测试 + 单一职责易于组合复用
    • 接口 in, 接口体 out

    Go 社区流传一个经验法则:“接受接口,返回结构体(Accept interfaces, return structs)

    • 尽量不要使用 any

    Go 语言之父 Rob Pike 曾说过:空接口不提供任何信息(The empty interface says nothing)

    写在最后

    得益于Go的核心理念和设计哲学:

    核心理念:简单、诗意、简洁(Simple, Poetic, Pithy)

    设计哲学:简单、显式、组合、并发和面向工程

    Go 拥有编程语言中相对最少的关键字和类型系统, 让Go入门变得极易学习和上手, 希望这blog能帮助入门的gopher, 更快掌握Go, 同时避免一些易错点

    相关文章

      网友评论

          本文标题:工作中用Go: Go基础

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