zoukankan      html  css  js  c++  java
  • Golang高效实践之泛谈篇

    前言

    我博客之前的Golang高效实践系列博客中已经系统的介绍了Golang的一些高效实践建议,例如:《Golang高效实践之interface、reflection、json实践》、《Golang 高效实践之defer、panic、recover实践》、《Golang 高效实践之并发实践context篇》、《Golang 高效实践之并发实践channel篇》,本文将介绍一些零散的Golang高效实践建议,虽然琐碎但是比较重要。

    建议

    1.代码格式go fmt工具,开发不用过多关注。

    2.支持块注释和行注释,一般包开头用块注释说明,函数用行注释说明,为了提高辨识度,函数注释一般以函数名为开头。例如:

    // Compile parses a regular expression and returns, if successful,
    
    // a Regexp that can be used to match against text.
    
    func Compile(str string) (*Regexp, error) {

    3.包名尽量简洁有意义,一般是一个小写单词,不需要下划线或者驼峰命名。不要用点号引进包,除非是为了简化单元测试。

    4.Go不提供getters和setters方法,用户要自己实现。例如有一个字段叫owner(小写,非导出变量),那么getter方法应该命名为Owner而不是GetOwner。如果需要setter方法应该命名为SetOwner。例如:

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

    5.接口命名在方法名后加er,例如:Reader,Writer,Formatter,CloseNotifier等等。

    6.变量命名用驼峰例如MixedCaps或者mixedCaps,不用下划线。

    7.Go和C一样是用分号作为语句的结束标记,不同的是Go是词法分析器自动加上去,不用程序员手动添加。词法分析器添加分号的标记一是行末遇到int或者float64等关键字类型,或者出现下面的特殊字符:

    break continue fallthrough return ++ -- ) }

    所以:

    if i < f() {
    
        g()
    
    }

    开括号‘{’要放在‘)’后面,否则词法分析器会自动在‘)’末尾添加分到导致语法错误。所以不能像下面这样写:

    if i < f()  // wrong!
    
    {           // wrong!
    
        g()
    
    }

    8.非必须的else可以省略,例如:

    if err := file.Chmod(0664); err != nil {
    
        log.Print(err)
    
        return err
    
    }

    9.声明和重新赋值:

    f, err := os.Open(name)

    该语句声明了f和err,紧接着:

    d, err := f.Stat()

    看着像声明了d和err,但实际上是声明了d,err是重新赋值。也就是说f.Stat用了上面已经存在的err,仅仅是重新给该err赋了一个新值。

    所以 v:= declaration是声明还是重新赋值取决于:

    1.该声明作用域已经存在一个已经声明的v,那么就是赋值(如果v已经在外面的作用域声明,那么这里会重新生成一个新的变量v)

    例如:

    package main
    
     
    
    import (
    
    "errors"
    
    "fmt"
    
    )
    
     
    
    func main() {
    
    fmt.Println(declareTest())
    
    }
    
     
    
    func declareTest() (err error){
    
    //declare a new variable err in if statement
    
    if err := hello(); err != nil {
    
    fmt.Println(err)
    
    }
    
    fmt.Println(err)
    
    return
    
    }
    
     
    
    func hello() error {
    
    return errors.New("hello world")
    
    }

    程序输出:

    hello world

    <nil>

    <nil> 

    2.如果是赋值,那么左边至少要有一个声明的新变量,否则会报语法错误。

    10.for循环。Go的for循环和C很像,但是不支持while循环。有以下三种形式:

    // Like a C for
    
    for init; condition; post { }
    
     
    
    // Like a C while
    
    for condition { }
    
     
    
    // Like a C for(;;)
    
    for { }

    也可以用for循环遍历数组、切片、字符串、map、或者读channel,例如:

    for key, value := range oldMap {
    
        newMap[key] = value
    
    }
    
    for pos, char := range "iam中国人" {
    
    fmt.Printf("character %#U start at byte position %d
    ", char, pos)
    
    }

    程序输出:

    character U+0069 'i' start at byte position 0
    
    character U+0061 'a' start at byte position 1
    
    character U+006D 'm' start at byte position 2
    
    character U+4E2D '' start at byte position 3
    
    character U+56FD '' start at byte position 6
    
    character U+4EBA '' start at byte position 9

    // Reverse a,翻转字符切片a

    for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    
        a[i], a[j] = a[j], a[i]
    
    } 

    11.switch。Go的switch比C灵活,case的表达式不要求一定是常量甚至整数,例如:

    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
    
    }

    每个case不会自动顺延到下一个case,如果需要顺延需要手动fall through。

    Switch用于类型判断:

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

    12.命名函数返回值。Go函数的返回值可以像输入函数一样命名(当然也可以不命名),命名返回值在函数开始时就已经被初始化为类型的零值。如果函数执行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
    
    }

    13.用defer释放资源,比如关闭文件、释放锁。这样做有两个好处,一是保证不会忘记释放资源,另外是释放的代码贴近申请的代码,更加清楚明了。更多defer特性请参考我的《Golang 高效实践之defer、panic、recover实践》博文。

    14.new(T)分配一个*T类型,指向被赋予零值的一块内存。例如:

    type SyncedBuffer struct {
    
        lock    sync.Mutex
    
        buffer  bytes.Buffer
    
    }
    
    p := new(SyncedBuffer)  // type *SyncedBuffer,相当于p:= &SyncedBuffer{}
    
    var v SyncedBuffer      // type  SyncedBuffer 

    15.构造函数。Go并没有像C++一样为每个类型提供默认的构造函数。所以当new(T)分配的零值不能满足我们要求时,我们需要一个初始化构造函数,一般命名为NewXXX,例如:

    func NewFile(fd int, name string) *File {
    
        if fd < 0 {
    
            return nil
    
        }
    
        f := new(File)
    
        f.fd = fd
    
        f.name = name
    
        f.dirinfo = nil
    
        f.nepipe = 0
    
        return f
    
    }

    也可以这样顺序初始化成员:

    return &File{fd, name, nil, 0}

    还可以指定成员初始化:

    return &File{fd: fd, name: name}

    所以new(File)是等于&File{}

    16.用make(T, args)创建切片、map、channel,返回已经初始化的(非零值)T类型(不是*T)。因为这三种数据结构必须在使用前完成初始化,例如切片的零值是nil,直接操作nil是会panic的。

    make([]int, 10, 100)

    分配一个length为10,capacity为100的切片。而new([]int)返回的值一个执行零值(nil)的切片指针。

    下面的示例会清楚的区分new和make的差别:

    var p *[]int = new([]int)       // allocates slice structure; *p == nil; rarely useful
    
    var v  []int = make([]int, 100) // the slice v now refers to a new array of 100 ints
    
     
    
    // Unnecessarily complex:
    
    var p *[]int = new([]int)
    
    *p = make([]int, 100, 100)
    
     
    
    // Idiomatic:
    
    v := make([]int, 100

    记住只有切片、map和channel分配用到make,并且返回的不是指针。

    17.数组。和切片不同,数组的大小是固定的,可以避免重新分配内存。和C语言数组不同的时,Go的数组是值,赋值时会引发数组拷贝。当数组作为参数传递给函数时,函数将会接受到数组的拷贝,而不是数组的指针。另外数组的大小也是数据类型的一部分。也就是说[10]int 和 [20]int不是同一种类型。

    但是值属性本身是效率比较低的,如果不能拷贝传递可以传递数组的指针,例如:

    func Sum(a *[3]float64) (sum float64) {
    
        for _, v := range *a {
    
            sum += v
    
        }
    
        return
    
    }
    
     
    
    array := [...]float64{7.0, 8.5, 9.1}
    
    x := Sum(&array)  // Note the explicit address-of operator

    但是这样不符合Go的编程习惯。这里可以用切片避免拷贝传递。

    18.切片。尽量用切片代替数组。切片本质是数组的引用,底层的数据结构还是数组。所以当把切片A赋值给切片B时,A和B指向的是同一个底层数组。当给函数传递切片时,相当于传递底层数组的指针。因此切片通常是更高效和常用。

    特别需要注意的是,切片的capacity也就是cap函数的返回值是底层数组的最大长度,当切片超过了改值时将会触发重新分配,底层的数组将会扩容,并且将之前的值拷贝到新内存中。

     
    
    func Append(slice, data []byte) []byte {
    
        l := len(slice)
    
        if l + len(data) > cap(slice) {  // reallocate
    
            // Allocate double what's needed, for future growth.
    
            newSlice := make([]byte, (l+len(data))*2)
    
            // The copy function is predeclared and works for any slice type.
    
            copy(newSlice, slice)
    
            slice = newSlice
    
        }
    
        slice = slice[0:l+len(data)]
    
        copy(slice[l:], data)
    
        return slice
    
    }

    Append函数最后要返回切片的值,因为切片(运行时持有指针,length和capacity的数据结构)本身是值传递的。

    19.二维切片。Go的数组和切片都是一维的,如果需要创建二维的数组或者切片则需要定义数组的数组,或者切片的切片。例如:

    type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
    
    type LinesOfText [][]byte     // A slice of byte slices.

    因为切片的长度是可变的,所以每个切片元素可以有不同的长度,所以有:

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

    需要注意的是,make只会初始化一维,二维的切片需要我们手动初始化,例如:

    // Allocate the top-level slice.
    
    picture := make([][]uint8, YSize) // One row per unit of y.
    
    // Loop over the rows, allocating the slice for each row.
    
    for i := range picture {
    
      picture[i] = make([]uint8, XSize)
    
    }

    20.map。map的key可以是任意定义了相等操作的类型,例如int,float,complex,字符串,指针,interface(只要是concrete type支持相等比较),结构体和数组。切片不能作为map的key,因为切片的相等没有定义。

    map可以按k-v的方式枚举初始化,例如:

    var timeZone = map[string]int{
    
        "UTC":  0*60*60,
    
        "EST": -5*60*60,
    
        "CST": -6*60*60,
    
        "MST": -7*60*60,
    
        "PST": -8*60*60,
    
    } 

    根据key索引value:

    offset := timeZone["EST"]

    当key不存在时,将会返回value对应的零值。例如:

    tm := make(map[string]bool)

    fmt.Println(tm["test"])

    将会输出false。那怎么区分究竟是key不存在还是key存在且本身value就是零值呢?可以这样利用“comma,ok”语法:

    var seconds int
    
    var ok bool
    
    seconds, ok = timeZone[tz]

    当key存在时ok为true,seconds为对应的value。否则ok为false,seconds为对应value的零值。 

    可以用delete指定map的key删除元素:

    delete(timeZone, "PDT")  // Now on Standard Time

    21.Go的格式输出是C语言风格的,但是比C的printf更高级。所有格式输出相关的函数在fmt包中,例如:fmt.Printf,fmt.Fprintf,fmt.Sprintf等等。例如:

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

    %v输出任意值:

    fmt.Printf("%v ", timeZone)  // or just fmt.Println(timeZone)

    程序结果:

    map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

    又例如:

    type T struct {
    
        a int
    
        b float64
    
        c string
    
    }
    
    t := &T{ 7, -2.35, "abc	def" }
    
    fmt.Printf("%v
    ", t)
    
    fmt.Printf("%+v
    ", t)
    
    fmt.Printf("%#v
    ", t)
    
    fmt.Printf("%#v
    ", timeZone)

    程序输出:

    &{7 -2.35 abc   def}
    
    &{a:7 b:-2.35 c:abc     def}
    
    &main.T{a:7, b:-2.35, c:"abc	def"}
    
    map[string]int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}

    %T输出类型:

    fmt.Printf("%T ", timeZone)

    运行结果:

    map[string]int

    %s调用类型的String()方法输出,所以不能在自定义类型的String()方法中使用%s,否则会死循环

    type MyString string
    
    func (m MyString) String() string {
    
        return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
    
    }

    修正版本:

    type MyString string
    
    func (m MyString) String() string {
    
        return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
    
    }

    22.append。append内建函数定义:

    func append(slice []T, elements ...T) []T

    T表示占位符,可以是任意的类型。编译时由编译器替换为实际的类型。用法:

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

    程序输出:[1 2 3 4 5 6]. 

    如果想将一个切片追加到另外一个切片末尾要怎么做呢?可以使用…语法,例如:

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

    如果没有…,编译将会不通过,因为y不是int类型。

    总结

    文章介绍了22个Golang的高效实践建议,其中包括一些编程规范和一些实践生产中容易遇到的坑,希望可以帮助到大家

    引用

    https://golang.org/doc/effective_go.html

  • 相关阅读:
    light-rtc: 理念与实践
    浅谈 WebRTC 的 Audio 在进入 Encoder 之前的处理流程
    倍频程与钢琴调式的距离
    实战排查|为什么遮挡推流摄像头,会导致播放绿屏?
    深入浅出 WebRTC AEC(声学回声消除)
    揭秘 VMAF 视频质量评测标准
    绕过CDN查找真实IP方法笔记
    Linux安全加固
    Windows安全加固
    常见端口渗透总结
  • 原文地址:https://www.cnblogs.com/makelu/p/11264932.html
Copyright © 2011-2022 走看看