golang-文章翻译-go高效编程(待补充)

原文地址

文章目录

背景

首先需要明确:要写好go ,绝对不是一件简单的事情,不是把 C++、Java 等其他语言的编码规范照搬过来就好的。go 自己有独特的命名、对象设计、程序构造等规范,只有按照go 本身的规范写好了,才能算把 go 这门语言写好了。
任何语言的学习过程其实都要大致经过 入门、了解高级用法、项目实践和性能优化 这几个过程。go官方的这篇文章基本把go语言原生特性都很好地讲了一遍,我们能在很多框架、优秀开源项目中看到这些用法,确实值得一学。

格式化

工具:go 自带的gofmt,使用参考博客

下面3种情况,gofmt 不会自动处理,需要开发自己留意:

1 分隔符
默认是tab,如果需要设置成空格,需要通过两个参数一起指定:
-tabwidth: 设置缩进空格数量,默认为8
-tabs: 是否使用tab 来表示缩进,默认为true,需要设置成false

2 单行长度不超过120
接下来会说到,go 的分号是编译器自动加上的,因此换行还不能随便换,gofmt 本身也不会帮我们处理行长度超长的问题,需要我们平时自己写代码的时候养成习惯,自己换行

3 括号
Go的设计思想中包括尽量减少括号,简化代码,因此空格有的时候是用来区分优先级的,比如:

x<<8 + y<<16

注释和godoc

godoc 使用参考博客
注释风格:和c++一致
特别注意:package comment
每一个包 都应该有对其基本介绍
但是只要在其中一个文件中写好就可以了
功能复杂的包 最好是有多行注释

参考:fmt包本身的注释
其他细节:
需要尽量保证注释本身的格式就是比较美观的,比如合理的空行、单行长度不超过120个字符等

必须要注释的:对外可见的方法、变量和属性

分组注释:同一类型的变量(比如错误码、枚举类型)可以放到一起,不加空行。第一行的注释也可以放简短描述,能够对所有变量生效
示例:

// Error codes returned by failures to parse an expression.
var (
    ErrInternal      = errors.New("regexp: internal error")
    ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
    ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
    ...
)

命名规范

一、包命名

1、路径规范
路径中应该全用小写,包括文件名本身

2、包命名细节规范
使用方import之后,可以使用最后一级,或者自己对包另外加别名,因此不需要担心最后一级重复的问题
不过也正因为如此,包的完整路径应该去体现包的完整功能。要简洁,但是不能不完整。
另外,也是因为使用包方法的时候要带上包的最后一级,所以 包内对象不应该再包含包名,比如 io.Reader 而不是 io.IOReader

二、getter

建议:Getter 前面最好不要带上Get, 直接用对象名即可,更加简洁

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}

三、Interface

和刚才Getter 的定义思路类似:Interface 中的方法定义,如果方法返回的对象非常明确,建议直接就使用对象名,而不要再加上表示动作的前缀。

另外,Interface 本身的命名也应该尽量简短,直接表示出这个对象是什么。

反例:所有的对象,都带上什么Interface、Controller 之类的后缀,导致对象类型本身非常长。如:SchoolController 。建议直接定义成:student.Controller

分号

从一个问题引入:go 确实不需要分号么?
如果你是使用goland 编写go 代码,你就可以发现,其实在行尾加上分号是不会编译操作的,只是会提醒:redundant semicolon
其实和C 语言一样,go 在编译的时候也是需要分号的,但是源代码中并不需要写,词法分析器(lexer)会自动帮我们加上
那么什么时候加呢?go lexer 会在结束符尾自动加上,常见的结束符有:

break continue fallthrough return ++ -- ) }

这的确帮我们节省了一些工作量,不过同样,这会导致go 对语法本身也是有一些要求的,比如 左大括号 必须写在行最后,不能新起一行,否则会导致上一行行尾 自动被加上分号,导致编译错误。

if i < f() {
    g()
} else {
    h()
}

条件控制语句

一、if

格式:建议多行、如果只是if 内部用,变量可以和if 语句在同一行初始化、尽量减少else 的使用(if 里面放异常情况,一般是直接退出)
示例代码:

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

二、再声明和再赋值(Redeclaration and Reassignment)

参考:考虑下面两行语句,第二行中的err 虽然是通过 := 设置的,但是也只是赋值

达到这种再赋值(reassignment) 需要2个条件:
1)之前已经声明过这个变量
2)重新赋值的时候至少还有另一个新创建的变量

示例:

f, err := os.Open(name)
...
d, err := f.Stat()

三、for

1、常见for 循环格式

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

2、使用下划线忽略不需要关注的对象

在编译map/slice 的时候还是很有用的,比如slice 大部分时候其实我们只关系值,不关心下标,就可以用 下划线 忽略下标:

sum := 0
for _, value := range array {
    sum += value
}

3、其他细节

for 循环遍历字符串,会按照具体编码的格式来展示,比如中文,就是一个个汉字;
++、-- 是语句而不是表达式,本身没有返回值

四、switch

1、特点

和 C 的switch 不同,go 的 switch 不仅可以写 bool 类型的表达式,还可以设置 equal 的条件,甚至还可以通过逗号分隔,用“或”的方式判断多个条件是否满足其一,如下:

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

2、switch 中的 break

由于 switch 中 走具体分支之后,其他的分支就不会走了(包括default),因此break 的使用场景其实不多,一般就用在刚才在介绍if 的时候说的:异常场景提前退出的时候才需要用到

3、实战:通过switch 实现更美观的字符串对比方法

这里其实就只是代码风格的问题了,见仁见智,如果有else if 这样的条件出现,确实switch 开起来会更美观一些,单个if…else 就不是特别必要

// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
func Compare(a, b []byte) int {
    for i := 0; i < len(a) && i < len(b); i++ {
        switch {
        case a[i] > b[i]:
            return 1
        case a[i] < b[i]:
            return -1
        }
    }
    switch {
    case len(a) > len(b):
        return 1
    case len(a) < len(b):
        return -1
    }
    return 0
}

4、实战:type switch

go 的type 由于也是一个变量,可以通过.(type) 的方式强转来获取。也正因为switch 可以放任何类型的变量,所以对type 的多分支判断也可以使用switch:

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

方法定义

一、多个返回值

背景:C语言去定义需要返回异常的场景的时候会遇到一个问题:比如从数据库获取一个实例对象,本来是可以直接返回这个对象,但是因为存在数据为空的情况,变成只能返回指针了。上层解析又要针对特殊情况做处理,以及对象转换。

Go的方法可以直接定义多个返回值,常见的格式如下:

func (file *File) Write(b []byte) (n int, err error)

代码规范:方法主要返回放在前面,错误信息error 放最后

也正是因为这种可以返回error的设计,go不需要异常处理机制(再结合刚才说的if 对错误的处理,以及后面要说到的defer recover)

除了返回 业务数据+error,还有一种场景是返回当前数据+下一个标志位,类似redis的 scanner
参考代码(最好用官方库的代码)

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ {
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

二、返回值命名

是一种设置返回值的特殊形式:不通过return,而是直接给返回值变量赋值。其实就是简化代码,比较取巧的一种方式,个人不是很推荐使用,加大了源代码理解成本,只有很特殊的场景才比较有可能用到

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

三、defer

最经典的用法就是资源释放,资源类对象在申请之后就紧接着defer,也是代码规范要求的

// 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()  // f.Close will run when we're finished.

    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.
}

其他使用场景:耗时计算(defer一个方法并执行)、调用链

特别注意: defer对方法传参的存储

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

最后输出:4,3,2,1,0
因为defer 存入是一个栈的模式,因此FILO(先进后出)

还有一点要留意:由于defer 中保留的局部变量是存值(其实和方法传参一样,指针类型就传地址),所以for 循环中释放局部变量对应的资源其实是不合理的,资源类对象往往都有指针类型的对象,for each 循环定义的都是同一个临时变量,因此可能会导致最后defer 释放的是同一个资源。
我们在最后讲channel 的时候还会回来看这个defer + channel 的用法,并说明正确的释放资源方式。

对象操作(声明、初始化等)

一、new

按传入的对象类型申请空间,并返回这个类型的对象对应的指针。
和直接通过var 初始化的结果一样,这个对象里面的成员都会被初始化成0值,指针类型的话是空,但是诸如sync包、bytes.Buffer对象,初始化0值之后是可以直接使用的,因为它们没有指针类型的属性。

比如下面这个syncedbuffer对象,new之后可以直接使用,内部对象不需要再初始化一样

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

// mutex
type Mutex struct {
    state int32
    sema  uint32
}

// buffer
type Buffer struct {
    buf      []byte
    off      int
    lastRead readOp(int8)
}

不过对于一些本身初始化就需要比较多参数的变量,还是应该通过var 方式初始化,相比new 来说,格式更简洁

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    return &File{fd, name, nil, 0}
}

二、make

New一般用来初始化struct对象,但是对于 slice、map和channel 这种容器对象来说,它们内部是有指针对象的,因此直接用new 初始化肯定不行,不能直接使用。需要通过make 来初始化
顺便来了解一下slice和map 的结构:

Slice 结构:
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

map 结构:
type hmap struct {
    count     int
    flags     uint8
    B         uint8
    noverflow uint16
    hash0     uint32

    buckets    unsafe.Pointer
    oldbuckets unsafe.Pointer
    nevacuate  uintptr

    extra *mapextra
}

make 的源码可以参考 makeslice、makemap,过程还是挺复杂的,感兴趣可以了解一下

三、数组(array)

数组相当于刚才看到的slice 数据结构中的内部对象 array

和slice 不同的地方:
1)必须在声明的时候初始化大小
2)不能扩容
3)对数组赋另一个数组,直接拷贝所有元素,两个数组的实际地址不同。同样传递到方法中也会拷贝一份
4)数组的大小在Type 中体现,因此[10]int 和 [20]int 类型不同,不能相互赋值

实际使用array 的场景其实不多,slice 更多一些(扩容更方便)
但是还是有一个跟GC 相关的优化细节:如果只要用到 超长Slice 的一部分元素,可以通过子array 来拷贝一份数组出来,而不是用子slice (子slice 会导致原大slice 依然被引用,不会被GC)
使用示例如下:

touselist = make([]int, 3)
copy(touselist, sublist)

四、slice

相比较array 在类型上的限制,slice 的使用就比较灵活了:不限大小、可以自动扩容、类型统一。因此go 底层传递数组 绝大多数都是 slice 实现的,而不是array

另外slice 是通过指针管理实际的数组的,因此slice 可以传递到方法中,并且方法内部对元素的修改在外部可见。

最后是 slice 最长用的append 方法,由于添加元素之后 slice 可能会扩容,导致后续的 slice 和原来的 slice 地址不同,因此需要接收 append 返回的新slice。
当然,效率更高的方式还是要预先给数组申请足够的capacity

arr = make([]int, 0, cap);
arr = append(arr, ele...)

五、二维数组

初始化:常见的方式依然是使用slice,只指定首层的大小,第二层先不初始化,同时这样每一层的数组大小也可以不同。
在 图像处理 类似的数据处理场景可以用到

text := LinesOfText{
    []byte("Now is the time"),
    []byte("for all good gophers"),
    []byte("to bring some fun to the party."),
}

六、map

map 也是go 里面的基本容器数据类型之一

1、key

类型可以是任何按 == 可比较的对象(比如基本类型、指针、interface 和 属性都是可比较的struct 其实都可以),slice 虽然可对比,但是== 其实是浅对比,并不是对比内部所有的元素,因此不适合作为key
注:切片 或者是包含了切片的 struct 如果要对比,可以使用reflect.DeepEqual 方法来比较,这个方法本质上就是对slice、array、struct 等复合结构体进行递归匹配所有属性是否相同,感兴趣可以直接看源码或者参考博客

2、添加value

map 虽然和slice 都是复合结构,但是和 slice 不同,扩容之后存储的地址还是不变的,因此可以放心地传递到方法内部并修改

3、获取

用法:和其他语言基本一致,也是分直接打印(print)、按format打印(printf)和指定流打印(fprint)这几种
因此如果value 是基础类型,不能直接从返回值直接确认到底key 是否存在,而应该通过这种方式判断:

_, ok := testMap[myFooVar]
if !ok {
  log.Printf("[test] get value failed")
}

或者是直接通过 if 判断:

if attended[person] { // will be false if person is not in the map
    fmt.Println(person, "was at the meeting")
}

更常见的其实是第一种,这种方式在go 中叫“comma ok”写法,类似的写法还有类型强转:

fooVar, ok := barVar.(Foo)

七、打印

1、基本用法

用法:和其他语言基本一致,也是分直接打印(print)、按format打印(printf)和指定流打印(fprint)这几种

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

其中,fPrint的第一个参数必须实现io.Writer接口

Printf和C语言的不同:由于go的对象类型是可以直接获取的,因此数字类型不需要指定长度。具体可参考 fmt/print.go 中的实现。

var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

所有对象都可以通过%v来format。对于struct类型的对象,默认打印其所有对象;

2、其他常用format:

%T:对象类型
%g:浮点数和整数,不设精度
%q:按各个字符打印字符串,不会按特殊字符截断,比如换行符也会按转义前格式来打印
%s:一般用来打印字符串或者是实现了String方法的struct,如果未实现就按照%v 方式打印对象。后面还会更详细讲到String 这种 pointer receiver的用法

3、小技巧:arbitrary type 的用法

来看看printf的方法定义:

func Printf(format string, v …interface{}) (n int, err error) {

其中,v就是arbitrary type,表示不定长参数,只能作为方法的最后一个参数传入
什么场景下会用到呢?比如printf 需要传递多个format参数,但是不确定参数个数,传递数组有点麻烦,在调用之前还得再多声明一个变量。
类似的,马上要说到的append方法也用到了这种类型。
到了方法内部,arbitrary type的用法就和数组没有区别了,可以直接用for遍历

4、扩展:日志包的选用

官方提供的log包没有日志等级区分,真的不能算好用。开源项目中推荐使用 uber/zap 来定制化自己的logger
参考博客

八、append

slice 专用的添加元素方法,直接看示例:

// example 1
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)

// example 2
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

初始化

1、常量(constant)

上一篇:js 数组方法的作用,各方法是否改变原有的数组


下一篇:jquery中,将a数组赋值给b,修改b中的值,不对a造成任何影响