78

GitHub - dolotech/ebook

 6 years ago
source link: https://github.com/dolotech/ebook
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go编码注意事项

  1. new和make的区别,前者返回的是指针,后者返回引用,且make关键字只能创建channel、slice和map这三个引用类型。

  2. 如果User结构想实现Test方法,以下写法:func (this *User) Test() ,User的实例和*User都可以调到Test方法,不同的是作为接口时User没有实现Test方法。

  3. interface 作为两个成员实现,一个是类型和一个值,var x interface{} = (*interface{})(nil) 接口指针x不等于nil 。下面一段代码深入展示:

type User struct {
    Id   int
    Name string
    Tester
}
type Tester interface {
    Test()
}
func (this *User) Test() {
	fmt.Println(this)
}
func create() Tester {
    var x *User = nil
    return x
}
func Test(t *testing.T) {
    var x Tester = create()
    if x != nil {
       t.Log("not nil ")
    }
    var u *User = x.(*User)
    if u == nil {
       t.Log("nil ")
    }
}
  1. 继承通过嵌入实现,也可说go语言没有继承语法。

  2. import关键字前面的"."和"_"的用法,点号表示调用这个包的函数时可以省去包名,下划线表示,纯引入包,因为go语法内没有使用这个包是不能导入的,包引入了,系统会自动调用包的init函数。

  3. select case必须是chan的io操作,为了避免饥饿问题,当多个通道都同时监听到数据时,select机制会随机性选择一个通道读取,一个通道被多个select语句监听时,同理。

  4. 关闭通道时所有select 监听都会收到通道关闭信号,某种意义上关闭通道是广播事件。

  5. goroutine的panic如果没有捕获,整个应用程序会crash ,所以安全起见每个复杂的go线都要recover。

  6. 在函数退出时,defer的调用顺序是写在后面的先被调用。

  7. init函数在main之前调用,被编译器自动调用,每个包理论上允许有多个init函数,编码上尽量避免同一个包内出现多个init函数。

  8. panic可以中断原有的控制流程,进入一个令人恐慌的流程中,这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。恐慌可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组. recover的用法,recover可以让进入令人恐慌的流程中的goroutine恢复过来。recover仅在defer函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。

  9. Array 和Slice的区别,Array就是一个数据块,值类型而非引用类型,传参时会进行内存拷贝,Slice是个reflect.SliceHeader结构体。Slice由make函数或者Array[:]创建。

  10. 闭包要注意循环调用时,upvalue值一不留意可能只是循环退出的值。如下代码:

func Test(t *testing.T) {
    var data int
    for i:= 0;i<10;i++{
    data ++
        go func(){
            listen2(data)
        }()
    }
    <- time.After(time.Second)
}
func listen2(data int) {
    fmt.Print( data)
}

输出:26101010101010106,跟你期望的输出可能不一样。

  1. 普通类型向接口类型的转换是隐式的,定义该接口变量直接赋值。接口类型向普通类型转换需要类型断言:value, ok := element.(T)。

  2. Go设计上模糊了堆跟栈的边界,go编译器帮程序员做了对象逃逸分析,优化了内存分配,t := T{}是可以在函数里返回的,并不是像C语言中在栈里分配内存了。

  3. 无论以接口或接口指针传递参数,接口指向的值都会被拷贝传递,引用类型(Map/Chan/Slice)拷贝该引用对象,值类型拷贝整个值(string除外)。

  4. go线程的调用时机是由go runtime决定的。

func Test(t *testing.T) {
	for i:= 0;i<10;i++{
		go listen2(i)
	}
	<- time.After(time.Second)
}
func listen2(data int) {
	fmt.Print( data)
}

输出:3456781209

  1. 调用log.Fatal系列函数后,会再调用 os.Exit(1) 退出程序,Fatal is equivalent to Print() followed by a call to os.Exit(1)

  2. 如果管道关闭则退出for循环,因为管道关闭不会阻塞导致for进入死循环,如下:

// 错误的做法
func Test_Select_Chan(t *testing.T) {
	readerChannel:= make(chan int )
	go func(readerChannel chan int ) {
		for {
			select {
			// 判断管道是否关闭
			case _, ok := <-readerChannel:
				if !ok {
					break
				}
			}
			t.Log("for")
		}
	}(readerChannel)
	close(readerChannel)
	<- time.After(time.Second*2)
}
// 正确的做法
func Test_Select_Chan1(t *testing.T) {
	readerChannel:= make(chan int )
	go func(readerChannel chan int ) {
		for {
			select {
			// 判断管道是否关闭
			case _, ok := <-readerChannel:
				if !ok {
					goto BB
					//return
				}
			}
			t.Log("for")
		}
	BB:
	}(readerChannel)
	close(readerChannel)
	<- time.After(time.Second*2)
}

for select 组合不带标签的break语法是跳不出循环,如果要跳出循环,要设置goto 标签或者直接return返回。

  1. map,slice,array,chan的数据存取值类型数据都是值拷贝赋值,这个跟很多脚本语言不同,一定要注意:
var list []mydata
var hash map[string]mydata
type mydata struct {
    A int
}
func Test(t *testing.T) {
	list = make([]mydata, 1)
	data := list[0]
	data.A = 10
	hash = make(map[string]mydata)
	hash["test"] = mydata{}
	data = hash["test"]
	data.A = 10
	t.Log(list[0].A, hash["test"].A)
}

输出:0 0

  1. 外部可见的属性必须是首字母大写,当转换到json数据是跟预期有偏差,必须添加json标签,如下:
type CMD struct{
        Cmd string `json:"cmd"`
        Data Data `json:"data"`
        UserId string `json:"userId"`
    }
  1. 包的循环引用编译错误,解决方法:提取公共部分到独立的包或者定义接口依赖注入。

  2. return XXX不是一条原子指令:

func Test(t *testing.T) {
	t.Log(test())
	t.Log(test1())
}
func test() (result int) {
	defer func() {
		result++
	}()
	return 1
}

func test1() (result int) {
	t := 5
	defer func() {
		t = t + 5
	}()
	return t
}

输出: 2 5

return XXX 不是一条原子指令,函数返回过程是,先对返回值赋值,再调用defer函数,然后返回调用函数 所以test方法的return 1可以拆分为:

result = 1
func()(result int){
    result ++
}()
return
  1. 内置copy方法,拷贝数组时,如果要整数组拷贝,目标数组长度要和源数组长度相同,否则剩下的数据不会被拷贝:
list := []int{12, 1242, 35, 23, 534, 23, 1}
listNew := make([]int, 1, len(list))
copy(listNew, list)
for i := 0; i < len(listNew); i++ {
fmt.Println(listNew[i])
}

输出:12

  1. 分割切片slice时,新切片引用的内存和老切片引用的是同一块内存:
list := []int{12, 1242, 35, 23, 534, 23, 1}
listNew := list[:3]
listNew2 := list[:5]
listNew[1] = 999
fmt.Println(listNew2[1])

输出:999

  1. 编译时设置编译参数去掉调试信息,可以让生成体积更小: go build -o target.exe -ldflags "-w -s" source.go

  2. Go语言里,string字符串类是不可变值类型,字符串的"+"连接操作、字符串和字符数组之间的转换string([]byte) 都会生成新的内存存放新字符串,当要对字符串频繁操作时做好先转换成字符数组。但是字符串作为参数传参时,此处go编译器作了优化,不会导致内存拷贝,引用的是同一块内存。benchmark如下:

func Benchmark_aa(b *testing.B) {
	very_long_string:= ""
	for i := 0; i < b.N; i++ {
		very_long_string += "test " + "and test "
	}
}
func Benchmark_bb(b *testing.B) {
	very_long_string:= []byte{}
	for i := 0; i < b.N; i++ {
		very_long_string = append(very_long_string,[]byte("test ")...)
		very_long_string = append(very_long_string,[]byte("and test ")...)
	}
}

输出: 200000 135817 ns/op 50000000 39.3 ns/op

  1. Go build/run/test 有个参数 -race ,设置-race运行时会进行数据竞态检测,并把关键代码打印输出,不过数据竞态不能全依赖race检测,不一定能全部检测出来。

  2. 如果非必要必要使用反射reflect和unsafe包内的函数,一定要使用时,要用runtime.KeepAlive函数告知SSA编译器在指定的代码段内不要回收该内存块。

  3. 不要打印整个map对象或者对象里有嵌套map的对象,打印函数会不加锁遍历map的每个元素,如果此时外部刚好有方法对map进行写操作,map就进入并发读写,runtime会panic。

  4. 注意range 循环迭代时key 的地址,for k,v:=range list 其中k 在迭代时指向同一个地址。

  5. append追加切片用法:

  • 如果slice还有剩余的空间,可以添加这些新元素,那么append就将新的元素放在slice后面的空余空间中
  • 如果slice的空间不足以放下新增的元素,那么就需要重现创建一个数组;这时可能是alloc、也可能是realloc的方式分配这个新的数组
  • 也就是说,这个新的slice可能和之前的slice在同一个起始地址上,也可能不是一个新的地址
  • 如果容量不足触发realloc,重新分配一个新的地址
  • 分配了新的地址之后,再把原来slice中的元素逐个拷贝到新的slice中并返回
  • 触发realloc时,容量小于1024,会扩展到原来的1倍,如果容量小大于1024,会扩展原来的1/4
  1. 很多打印函数打印结构体时回调用该结构体的String方法,所以String不能再打印本身这个对象。如下:
type S struct {
}
func (this S)String()  string{
	return fmt.Sprint( "S struct: ",this )
}
func Test_print(t *testing.T)  {
	var s S
	t.Log(s.String())
}

34.循环语句里正整型迭代值边界问题,迭代值边界递减到负值,下面的代码会进入死循环:

for i:= uint8(10);i>=0;i--{
	t.Log(i)
}
  1. recover只处理本goroutine调用栈,goroutine的panic如果没有捕获,整个应用程序会crash ,所以安全起见每个复杂的go线都要recover。

  2. map 并发读写的错误无法用panic捕获。

  3. 对于小对象,直接将值类型的对象交由 map 保存,远比用该对象指针高效。这不但减少了堆内存分配,关键还在于垃圾回收器不会扫描非指针类型 key/value 对象。

  4. Go 使用 channel 实现 CSP 模型。处理双方仅关注通道和数据本身,无需理会对方身份和数量,以此实现结构性解耦。在各文宣中都有 “Don't communicate by sharing memory, share memory by communicating.” 这类说法。但这并非鼓励我们不分场合,教条地使用 channel。在我看来,channel 多数时候适用于结构层面,而非单个区域的数据处理。原话中 “communicate” 本就表明一种 “message-passing”,而非 “lock-free”。所以,它并非用来取代 mutex,各自有不同的使用场景。

  5. 变量逃逸和函数内联状态分析 go build -gcflags "-m" -o main.exe main.go

  6. go汇编指令 go build -gcflags "-N -l" -o main.exe main.go && go tool objdump -s "main\.main" main.exe 关闭内联优化:go build -gcflags "-N -l"

  7. 关于defer机制,编译器通过 runtime.deferproc “注册” 延迟调用,除目标函数地址外,还会复制相关参数(包括 receiver)。在函数返回前,执行 runtime.deferreturn 提取相关信息执行延迟调用。这其中的代价自然不是普通函数调用一条 CALL 指令所能比拟的,单个函数里过多的 defer 调用可尝试合并。最起码,在并发竞争激烈时,mutex.Unlock 不应该使用 defer,而应尽快执行,仅保护最短的代码片段。

  8. 对map预设容量,map会按需扩张,但须付出数据拷贝和重新哈希成本。如有可能,应尽可能预设足够容量空间,避免此类行为发生。

  9. go语言调用c语言:

  • import "C" 这句代码必须紧跟伪注释的C语言代码后面不能有换行
  • go语言的CGO会自动链接编译.c文件,但是必须用go build 编译指令,不能指定main.go文件
  1. Go语言命名规范:
  • 文件命名,全小写+下划线
  • 结构体名字、变量名字采用驼峰命名(根据包外可见性确定首字母是否大写)
  • 常量命名,全大写 +下划线
  1. 删除切片的指定下标元素
  • 低效的做法
		for j := 0; j < len(array); j++ {
			if INDEX == j {
				array = append(array[:j], array[j+1:]...)
				break
			}
		}
  • 高效的做法
		
			for j :=INDEX; j < len(array)-1; j++ {
				array[j] = array[j+1]
			}
			array = array[:len(array)-1]
	

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK