Golang 知识累积

1,725 阅读9分钟

Go 开发技巧

1.异常处理统一使用error,不要使用panic/recover来模拟throw…catch,最初我是这么做的,后来发现这完全是自以为是的做法。

2.原生的error过于简单,而在实际的API开发过程中,不同的异常情况需要附带不同的返回码,基于此,有必要对error再进行一层封装。

3.任何协程逻辑执行体,逻辑最开始处必须要有defer recover()异常恢复处理,否则goroutine内出现的panic,将导致整个进程宕掉,需要避免部分逻辑BUG造成全局影响。

4.在Golang中,变量(chan类型除外)的操作是非线程安全的,也包括像int这样的基本类型,因此并发操作全局变量时一定要考虑加锁,特别是对map的并发操作。

5.所有对map键值的获取,都应该判断存在性,最好是对同类操作进行统一封装,避免出现不必要的运行时异常。

6.定义slice数据类型时,尽量预设长度,避免内部出现不必要的数据重组。

  1. package 的名字和目录名一样,main 除外.

  2. string 表示的是不可变的字符串变量,对 string的修改是比较重的操作,基本上都需要重新申请内存,如果没有特殊需要,需要修改时多使用 []byte.

  3. 尽量使用 strings 库操作 string,这样做可以提高性能.

  4. append 要小心自动分配内存,append 返回的可能是新分配的地址.

  5. 如果要直接修改 map 的 value 值,则 value 只能是指针,否则要覆盖原来的值

  6. map 在并发中需要加锁.

  7. 编译过程无法检查 interface{} 的转换,只有运行时检查,小心引起 panic.

  8. 使用 defer,保证退出函数时释放资源.

  9. 尽量少用全局变量,通过参数传递,使每个函数都是“无状态”的,这样减少耦合,也方便分工和单元测试.

  10. 参数如果比较多,将相关参数定义成结构体传递.

  11. unsafe

unsafe.Sizeof(i) 查看变量i占多少个字节

unsafe.Pointer 将[]bytestring:
*(*string)(unsafe.Pointer(&buf))
  1. context 当传递context时,可以设置超时:
ctx, cancel := context.WithTimeout(context.Background(), 1000 *time.Millisecond)
	defer cancel()

19.Chan 通过判断jobChan是否已经满了,Chan满了之后,会阻塞chan,通过以下方法,阻塞立即返回false.

func TryEnqueue(job Job, jobChan <-chan Job) bool {
    select {
    case jobChan <- job:
        return true
    default:
        return false
    }
}

20.判断某个类型,若是,则生成付给新的变量,ok=true.如:

tcpConn, ok := conn.(*net.TCPConn)

21.得到TCPConn对象,同时可以设置连接超时

d := net.Dialer{Timeout: timeout}
conn, err := d.Dial("tcp", addr)
if err != nil {
 // handle error
}

Go 规范指南(转)

  1. 写完代码都必须格式化,保证代码优雅:gofmt goimports.

  2. 编译前先执行代码静态分析:go vet pathxxx/

  3. package 名字:包名与目录保持一致,尽量有意义,简短,不和标准库冲突, 全小写,不要有下划线.

  4. 竞态检测:go build –race (测试环境编译时加上 -race 选项,生产环境必须去掉,因为 race 限制最多 goroutine 数量为 8192 个)

  5. 每行长度约定:一行不要太长,超过请使用换行展示,尽量保持格式优雅;单个文件也不要太大,最好不要超过 500 行.

  6. 多返回值最多返回三个,超过三个请使用 struct.

  7. 变量名采用驼峰法,不要有下划线,不要全部大写

  8. 在逻辑处理中禁用 panic,除非你知道你在做什么

  9. 错误处理的原则就是不能丢弃任何有返回 err的调用,不要采用_丢弃,必须全部处理。接收到错误,要么返回 err,要么实在不行就 panic,或者使用 log 记录下来。 不要这样写:

if err != nil {

    // error handling

} else {

    // normal code

}

而应该是:

if err != nil {

    // error handling

    return // or continue, etc.
}

// normal code...
  1. 常用的首字母缩写名词,使用全小写或者全大写,如 UIN URL HTTP ID IP OK

  2. Receiver::用一两个字符,能够表示出类型,不要使用 me self this

  3. 参数传递:

    a.对于少量数据,不要传递指针
    b.对于大量数据的 struct 可以考虑使用指针
    c.传入参数是 map,slice,chaninterfacestring 不要传递指针
    
  4. 每个基础库都必须有实际可运行的例子, 基础库的接口都要有单元测试用例

  5. 不要在 for 循环里面使用 defer,defer只有在函数退出时才会执行

  6. panic 捕获只能到goroutine最顶层,每个自己启动的goroutine,必须在入口处就捕获panic,并打印出详细的堆栈信息

  7. Go 的内置类型slice、map、chan都是引用,初次使用前,都必须先用 make 分配好对象,不然会有空指针异常

  8. 使用 map 时需要注意:map 初次使用,必须用 make 初始化;map 是引用,不用担心赋值内存拷贝;并发操作时,需要加锁;range 遍历时顺序不确定,不可依赖;不能使用 slice、map 和 func 作为 key

  9. import 在多行的情况下,goimports 会自动帮你格式化,但是我们这里还是规范一下 import 的一些规范,如果你在一个文件里面引入了一个 package,还是建议采用如下格式:

import (
    "fmt"
)

如果你的包引入了三种类型的包,标准库包,程序内部包,第三方包,建议采用如下方式进行组织你的包:

import (
    "encoding/json"
    "strings"

    "myproject/models"
    "myproject/controller"
    "myproject/utils"

    "github.com/astaxie/beego"
    "github.com/go-sql-driver/mysql"
)   

有顺序的引入包,不同的类型采用空格分离,第一种实标准库,第二是项目包,第三是第三方包。

  1. 如果你的函数很短小,少于 10 行代码,那么可以使用,不然请直接使用类型,因为如果使用命名变量很容易引起隐藏的 bug。 当然如果是有多个相同类型的参数返回,那么命名参数可能更清晰:
func (f *Foo) Location() (float64, float64, error)...
  1. 长句子打印或者调用,使用参数进行格式化分行 我们在调用 fmt.Sprint 或者 log.Sprint 之类的函数时,有时候会遇到很长的句子,我们需要在参数调用处进行多行分割:

下面是错误的方式:

log.Printf(“A long format string: %s %d %d %s”, myStringParameter, len(a),
    expected.Size, defrobnicate(“Anotherlongstringparameter”,
        expected.Growth.Nanoseconds() /1e6))

应该是如下的方式:

log.Printf( 
    “A long format string: %s %d %d %s”, 
    myStringParameter,
    len(a),
    expected.Size,
    defrobnicate(
        “Anotherlongstringparameter”,
        expected.Growth.Nanoseconds()/1e6, 
    ),
)...
  1. 注意闭包的调用 在循环中调用函数或者 goroutine 方法,一定要采用显示的变量调用,不要再闭包函数里面调用循环的参数
fori:=0;i<limit;i++{
    go func(){ DoSomething(i) }() //错误的做法
    go func(i int){ DoSomething(i) }(i)//正确的做法
}
  1. recieved 是值类型还是指针类型 到底是采用值类型还是指针类型主要参考如下原则:
func(w Win) Tally(playerPlayer)int    //w不会有任何改变 
func(w *Win) Tally(playerPlayer)int    //w会改变数据
  1. struct 声明和初始化格式采用多行: 定义如下:
type User struct{
    Username  string
    Email     string
}

初始化如下:

u := User{
    Username: "astaxie",
    Email:    "astaxie@gmail.com",
}
  1. 变量命名

    (1).和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写,但遇到特有名词时,需要遵循以下规则:

    a. 如果变量为私有,且特有名词为首个单词,则使用小写,如 apiClient
    b. 其它情况都应当使用该名词原有的写法,如 APIClient、repoID、UserID
    c. 错误示例:UrlArray,应该写成 urlArray 或者 URLArray
    

    (2).若变量类型为 bool 类型,则名称应以 Has、Is、Can 或 Allow 开头

var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
  1. 常量命名 常量均需使用全部大写字母组成,并使用下划线分词
const APP_VER = "1.0"...

golang 底层知识点

1. waitGroup-warp

warp自动为匿名函数Add,当函数结束后Done.warp第一个参数是匿名函数它得到参数是argvs ...interface{};后面跟着这个匿名函数的参数.

func (w *WaitGroupWrapper) Wrap(cb func(argvs ...interface{}), argvs ...interface{}) {
    w.Add(1)
    go func() {
        cb(argvs...)
        w.Done()
    }()
}

2. sync.Pool 临时对象池

  1. 增加对象重用的几率,减少gc的负担,而开销方面也不是很便宜的。

  2. 保存在Pool中的对象会在没有任何通知的情况下被自动移除掉。实际上,这个清理过程是在每次垃圾回收之前做的。垃圾回收是固定两分钟触发一次。而且每次清理会将Pool中的所有对象都清理掉!

  3. Go语言的goroutine虽然可以创建很多,但是真正能物理上并发运行的goroutine数量是有限的.所以Pool是给每个线程分配了一个poolLocal对象是,也就是说local数组的长度,就是工作线程的数量(size :=runtime.GOMAXPROCS(0))。其决定了。这个Pool高效的设计的地方就在于将数据分散在了各个真正并发的线程中,每个线程优先从自己的poolLocal中获取数据,当自己线程的poolLocal中没有数据时,才会尝试加锁去其他线程的poolLocal中“偷”数据,很大程度上降低了锁竞争。 

3. context

注意: 使用时遵循context规则

  1. 不要将 Context放入结构体,Context应该作为第一个参数传入,命名为ctx。
  2. 即使函数允许,也不要传入nil的 Context。如果不知道用哪种Context,可以使用context.TODO()。
  3. 使用context的Value相关方法,只应该用于在程序和接口中传递和请求相关数据,不能用它来传递一些可选的参数.
  4. 相同的 Context 可以传递给在不同的goroutine;Context 是并发安全的。

用法:

WithCancel

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers

//cancel之后,ctx会穿ctx.Done()
case <-ctx.Done():

WithDeadline

//定时在某个时刻
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()

select {
case <-time.After(1 * time.Second):
	fmt.Println("overslept")
case <-ctx.Done():
    fmt.Println(ctx.Err())
}

WithTimeout

//设置超时
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

select {
case <-time.After(1 * time.Second):
    fmt.Println("overslept")
case <-ctx.Done():
	fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}

WithValue


//设置某个键值对到context里面
        type favContextKey string

	f := func(ctx context.Context, k favContextKey) {
		if v := ctx.Value(k); v != nil {
			fmt.Println("found value:", v)
			return
		}
		fmt.Println("key not found:", k)
	}

	k := favContextKey("language")
	ctx := context.WithValue(context.Background(), k, "Go")

	f(ctx, k)
	f(ctx, favContextKey("color"))