GoLang 最佳实践超全版【一】

一、入门

二、初始化

展开
  1. 常量
type ByteSize float64

const (
    _            = iota //  iota 是枚举器(0 开始,每行+1),用 _ 忽略第一个值
    KB ByteSize  = 1 << (10 * iota)
    MB
    GB
)
  1. 变量
x := 10
var (
    home = os.Getenv("HOME")
    user = os.Getenv("USER")
)
  • 如果左边多个变量,可以有已经定义过的变量(同一作用域里),但必须至少有一个是未定义过的。对于已经定义过的变量就是赋值操作。
var err error.Error
res, err := get()
  • 如果是在全局区域,只能用 var 初始化,不能用 :=
  • 会让可读性变差、带来意料之外的副作用。
  • 最好是提供相应初始化函数给调用者来避免使用init。
  • 避免I/O调用(单元测试时不希望有)、避免依赖其它的 init() 函数、避免修改全局函数、环境状态。
func init() {
    这里执行的代码会在当前包被 import 时执行。
}

三、数据

展开

1. 基本类型

  • int int8 int16 int32 int64
  • uint uint8 uint16 uint32 uint64 uintptr
  • bool string byte(uint8别名) rune(int32 别名)
  • float32 float64 complex64 complex128

2. 结构体

展开
// 这样初始化拿到的是该类型的零值的指针,里面每个属性都是对应类型的零值
f := &File{} 或 f := new(File)
f.fd = fd
f.name = name

// 填写每个属性
f := &File{fd, name, nil, 0}

// 通过 label 可以只赋值部分属性
f := &File{fd: fd, name: name}

3. array 和 slice

展开
  1. array 的定义方式
a := [100]int{}
var b [10]int
c := [...]int{1, 2, 3}
  1. slice 的定义方式
s := make([]int, 100)
var nums []float
  1. 二维数组/切片
var s [][]byte

// 定义了一个 5 行 8 列的数组
picture := make([][]uint8, 5)
for i := range picture {
    picture[i] = make([]uint8, 8)
}
  1. 可以通过 type 定义一个类型
type LinesOfText [][]byte
text := LinesOfText{
    []byte("good"), 
    []byte("gophers"),
}
  1. 整体赋值
// 数组整体赋值是拷贝
a := [1]int{1}
b := a // 此时 a 和 b 不是同一个数组
b[0] = 2 // a = [1], b = [2]

// slice 的整体赋值是指向同一个底层数组
s := []int{1}
t := s
t[0] = 2 // s = [2], t = [2]
  1. 切片操作

起点可以省略,不能像 python 一样用负数。切片底层指向同一块空间。

s := a[1:3]
prefix := str[:len(str)-10]
  1. 完整的切片表达式

代表容量(cap)为 max-low,容量会影响 append 操作的具体实现。字符串不适用。

a[low : high : max]

4. map

展开
  1. 定义与使用
var timeZone = map[string]int{
    "UTC": 0,
    "CST": -6,
    "PST": -8,
}

timeZone["MST"] // 0,不存在的 key,会得到 value 类型的零值。
t, ok := timeZone["MST"] // 会得到 0, false
if _, ok := timeZone["MST"]; ok { // 如果只是判断是否存在

delete(timeZone, "UTC") // 删除某个 key
  1. Map 的 value 存的是值,会发生 copy,所以一般用指针类型。
  2. Map 的 key 不应用指针类型,否则值相同也是不同 key。
  3. slice、function、map 类型不能作为 map 的 key。
  4. 不能对 map 的 value 进行取地址,因为一扩容地址就变了。
&timeZone["UTC"] // 是错误的, cannot take address of timeZone["UTC"]
  1. 不能对 map 的值的字段修改,除非是指针类型。
myMap := map[string]Point{"origin": {x: 0, y: 0}}
myMap["origin"].x = 3 // 是错误的。cannot assign to struct field .. in map

myMap := map[string]*Point{"origin": {x: 0, y: 0}}
myMap["origin"].x = 1 // 是可以的
  1. map 不支持并发读写,只能并发读。并发写回强行退出程序,抛出 fatal error: concurrent map read and map write。
  2. 删除 key 时,Map 不会自动缩容,需要设置为 nil,或是手动调用 gc。
runtime.GC()
  1. 避免在map 的值中引用大数组
name := "/path/to/file"
data, _ := ioutil.ReadFile(name)
myMap[name] = data[:10] // Bad:data 不会被回收
myMap[name] = append([]byte{}, data[:10]...) //  Good:data 会被回收

5. string

展开
  1. 底层实现是一个指向数据的指针和一个长度字段。
  2. String 和 []byte 直接做类型转换会进行拷贝。
  3. 中文的 string 切片或获取长度时应转为 []rune 再使用。

6. set

展开

Go 中没有集合,可以用 map 实现,并用struct{} 作为值,因为它被编译器优化过,指向同一个内存地址,不额外占空间。

type MySet map[int]struct{}

func (s MySet) Add(num int) {
    if s == nil {
        s = make(MySet)
    }
    s[num] = struct{}{}
}

set := MySet{}
set.Add(1)

四、结构

展开
  1. 如果只是 if 里用的变量,尽量写在 if 的初始化里,代码看起来更简洁
if r, ok := dst.(ReaderFrom); ok {
   return r.ReadFrom(src)
}
  1. for 循环
展开
for init; condition; post {}

// 相当于 while
for condition {}

// 相当于 for(;;)
for {}

// 可以这样初始化、改变多个变量,但是不能像 C++ 里用`,`划分多个语句。
for i, j := 0, len(a) - 1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

// 遍历的是 key
for x := range myMap {}

// 遍历的是 value
for _, v := range myMap {}

// 遍历的是 index
for x := range mySlice {}

// 遍历的是 value
for _, v := range mySlice {}
  1. switch
展开
  1. 可以不根据某个变量的值进行选择,而是从上到下找到第一个为真的表达式
func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}
  1. 不会像 C++ 一样自动进入下一个 case,如果需要可以用 fallthrough 关键字,或者
switch c {
case ' ', '?', '&':
    f(c)
}
  1. 使用 label 跳出循环(适用于 for 循环里的 switch、select 代码块)
Loop:
    for {
        r, size := decode(s[i:])
        switch {
        case size == 1 && r == invalidMsg:
            return "", fmt.Errorf("invalid: %q", s) // %q 带双引号

        case size == 0:
            // 跳出 for 循环
            break Loop

        default:
            result += r
        }
        i += size
    }
  1. interface{} 的类型判断
func f(x int) interface{} {
    switch x {
    case 1:
        return 1
    case 2:
        return "1"
    }
    return nil
}

t = f(1)
switch t := t.(type) {
default:
    fmt.Printf("unexpected type: %T\n", t)
case int:
    fmt.Printf("integer %d\n", t)
case string:
    fmt.Printf("string %s\n", t)
}
}

五、函数

展开

1. 返回多个值

展开

多个返回值需要用括号括起来。
返回值可以有名字,初始值为类型的零值。return 可后面不加内容。

func Cut(s, sep []byte) (before, after []byte, found bool) {
        if i := Index(s, sep); i >= 0 {
                return s[:i], s[i+len(sep):], true
        }
        return s, nil, false
}

2. 闭包

展开
  1. 引用了函数体外面的变量的函数。也就是这些变量和该函数绑定在一起。
展开
func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

pos, neg = adder(), adder()
for i := 0; i < 10; i++ {
    fmt.Println(pos(i), net(-i))
}
  1. for 循环里用闭包的坑(govet 会检测出来)
展开
for i := 0; i < 100; i++ {
    go func() {
        fmt.Println(i)
    }()
}
select{} // 阻塞

会得到100 个 0-100 中的任意数字。应该改为

go func(num int) {
    fmt.Println(num)
}(i) // 将变量拷贝传进函数

会得到 0-100 的某个排列

另一种方法是

for i := 0; i < 100; i++ {
    ii := i // 局部变量逃逸,指的是编译器会根据实际使用域决定放堆上还是栈上。这里会放到堆上。
    go func() {
        fmt.Println(ii)
    }()
}

3. defer

展开
  1. 常用来解锁、关闭连接、关闭文件描述符、recover panic。
展开
// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // Close 应该放到 err 判断后面,否则会 panic

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}
  1. 按栈的顺序,先进后出。
  2. 预计算参数
展开
func f() {
    startedAt := time.Now()
 
    // 这样会立刻计算出函数用到的参数并进行拷贝,而不是执行 defer 时才计算。
    defer fmt.Println("假执行时间: ", time.Since(startedAt))
    
    // 利用闭包。这样拷贝的是函数指针。退出函数 f 时才计算。
    defer func() { fmt.Println("真执行时间: ", time.Since(startedAt)) }()
        
    time.Sleep(time.Second)
}
  1. 注意 defer 属于哪个函数
展开
func f() {
    for {
        func(..) {
            row, err := db.Query("...")
            if err != nil {
                ..
            }
            defer row.Close() // 这样会在内层函数退出时就执行
            ..
        }(..)
    }
}
  1. 函数也可以作为类型,传参
展开
func compute(f func(float64, float64) float64) float64 {
    return f(3, 4)
}

add := func(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
}

compute(add)

4. make

展开
// slice 的初始化,会有一个容量是 cap,有 len 个 0 (int 的零值)的 int 数组。
s := make([]int, len, cap)

// len 和 cap 相等时可以忽略 cap,这样更简洁
s := make([]int, len)

// map 的初始化
m := make(map[string]string, cap)
  • 对于最大长度确定的时候,初始化 len,通过下标修改值,会比 append 更高效。

5. append

展开
// append slice。数组不能 append。
func append(slice []T, elements ...T) []T

// 可以 append 多个
s = append(s, 0, 1, 2)

// append 后 len 就是原来的 len 加上新元素个数。
// 扩容逻辑:cap < 1024时,会翻倍增长,否则 1.25 倍。
var s []int
s = append(s, 0) // len 是 1, cap 是 1
s = append(s, 1) // len 是 2, cap 是 2
s = append(s, 2) // len 是 3, cap 是 4
fmt.Println(s) // [0, 1, 2]


func modifySlice(s []int) {
    s = append(s, 3)
    s[0] = -1
}

// 函数都是传值的,slice 的数据结构里包含指向数组的指针、len、cap。
modifySlice(s)
fmt.Println(s) // [-1, 1, 2],append 修改了 cap 和 len 不会作用在这里的 s

六、方法

展开
  1. 可以给结构体定义方法。接收者可以用值或指针。
// 不修改函数接收者时,用值作为函数接收者
func (r Response) Code() int {
    return r.code
}

// 修改时,用指针。但是有时为了统一写法,就都用指针。
func (r *Response) SetCode(code int) {
    r.code = code
}

// 使用值接收者的函数可以被指针、值调用。而指针接收者只能被指针调用。
var r *Response
r.SetCode(1)
r.Code()
  1. 为结构体实现 String 方法,方便打印,尤其是带有指针成员的结构体。
type Point struct {
    x, y int
}

func (p *Point) String() string {
    return fmt.Sprintf("(%d, %d)", p.x, p.y)
}

myMap := map[string]*Point{"origin": {x: 0, y: 0}}
fmt.Printf("%s", myMap)

七、接口

展开 1. 定义接口
type Writer interface {
  
}
  1. 一个类型可以实现多个接口。

如下是实现 sort.Interface 接口,需要实现 Len(), Less(), Swap() 函数

type Sequence []int

// Methods required by sort.Interface.
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
    copy := make(Sequence, 0, len(s))
    return append(copy, s...)
}

// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
    s = s.Copy() // Make a copy; don't overwrite argument.
    
    sort.Sort(s)
    // 也可以直接 sort.IntSlice(s).Sort(),就不用自己实现接口了。
    
    return fmt.Sprint([]int(s))
}
posted @ 2022-04-25 21:00  水郁  阅读(336)  评论(0编辑  收藏  举报
……