Go 的优点
Go like C++
- 内存消耗少
- 执行速度快
- 启动快 Go not like C++
- 程序编译时间短
- 像动态语言一样灵活(runtime, interface, 闭包,反射)
- 内存并发正常
defer
defer 执行顺序是先进后出,栈的方式 FIFO ,参数的值在 defer 语句执行就已经确定了
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
执行结果:
4 3 2 1 0
append 不是线程安全的
slice 中,如果 a[x] 和 b[y] 指向同一个内存区域,那么存在竞态关系
package main
import (
"fmt"
)
func main() {
a := []int{1, 2}
b := a[1:]
go func() {
a[1] = 0
}()
fmt.Println(b[0])
}
slice.go
package main
import (
"fmt"
)
func main() {
a := []int{1, 2}
b := a[1:]
go func() {
a[1] = 0
}()
fmt.Println(b[0])
}
运行命令
go run -race slice.go
执行结果
2
==================
WARNING: DATA RACE
Write at 0x00c0000bc018 by goroutine 7:
main.main.func1()
/Users/bytedance/go/src/code.byted.org/wangmingming.hit/GoProject/main/slice.go:11 +0x47
Previous read at 0x00c0000bc018 by main goroutine:
main.main()
/Users/bytedance/go/src/code.byted.org/wangmingming.hit/GoProject/main/slice.go:14 +0xb9
Goroutine 7 (running) created at:
main.main()
/Users/bytedance/go/src/code.byted.org/wangmingming.hit/GoProject/main/slice.go:10 +0xab
==================
Found 1 data race(s)
exit status 66
零值
零值和未出世后的值并不相同, 不同类型的零值是什么
- 布尔类型是 false, 整型是0, 字符串是 “”
- 指针,函数,interface 、slice 、channel 和 map 的零值都是 nil
- 结构体的零值是递归生成的,每个成员都是对应的零值
使用要注意如下几点:
- 一个为nil的slice,除了不能索引外,其他的操作都是可以的
- nil的map,我们可以简单把它看成是一个只读的map
// 一个为nil的slice,除了不能索引外,其他的操作都是可以的
// Note: 如果这个slice是个指针,不适用这里的规则
var a []int
fmt.Printf("len(a):%d, cap(a):%d, a==nil:%v\n", len(a),cap(a), a == nil) //0 0 true
for _, v := range a{// 不会panic
fmt.Println(v)
}
aa := a[0:0] // 也不会panic,只要索引都是0
// nil的map,我们可以简单把它看成是一个只读的map
var b map[string]string
if val, ok := b["notexist"];ok{// 不会panic
fmt.Println(val)
}
for k, v := range b{// 不会panic
fmt.Println(k,v)
}
delete(b, "foo") // 也不会panic
fmt.Printf("len(b):%d, b==nil:%v\n", len(b), b == nil) // 0 true
值传递
Go 语言中所有的传参都是值传递,或者说一个拷贝,传入的数据能不能在函数内被修改,取决于是指针或者含有指针的类型(指针被值传递复制后依然指向同一块地址),什么时候传入的参数会修改会生效,什么时候不会生效。 slice类型在 值传递的时候len和cap不会变,所以函数内append没有用:
type slice struct {
array unsafe.Pointer
len int
cap int
}
// badcase
func appendMe(s []int){
s = append(s, -1)
}
map 和 chan 是引用类型,是个指针, 在函数内修改会生效
// map实际上是一个 *hmap
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
//省略无关代码
}
// chan实际上是个 *hchan
func makechan(t *chantype, size int64) *hchan {
//省略无关代码
}
结构体传参
// 这是一个典型的指针包裹类型
type Person struct {
name string
age *int
}
func modify(x Person){
x.name = "modified"
*x.age = 66
}
这个结构体中 age 是个指针类型,在函数内会被修改
复制数据时,使用 copy 比 append 性能更好
import (
"crypto/rand"
"testing"
)
var (
src = make([]byte, 512)
dst = make([]byte, 512)
)
func genSource() {
rand.Read(src)
}
func BenchmarkCopy(b *testing.B) {
for n := 0; n < b.N; n++ {
b.StopTimer()
genSource()
b.StartTimer()
copy(dst, src)
}
}
func BenchmarkAppend(b *testing.B) {
for n := 0; n < b.N; n++ {
b.StopTimer()
genSource()
b.StartTimer()
dst = append(dst, src...)
}
}
dst 作为全局变量防止编译器优化 for-loop
uptime;go version;go test -bench=. ./
11:56:10 up 294 days, 14:58, 3 users, load average: 0.58, 0.52, 0.63
go version go1.14.1 linux/amd64
goos: linux
goarch: amd64
pkg: copyvsappend
BenchmarkCopy-40 9808320 116 ns/op
BenchmarkAppend-40 479055 8740 ns/op
PASS
Go 语言中为啥没有继承
go 没子类型的概念,只能把类型嵌入另外一个类型中,所以没有类型系统。
- 使用伸缩性良好的组合,而不是继承
- 数据和方法不绑定在一起,数据的集合使用 struct, 方法的集合使用 interface ,保持正交
接收器是用指针还是值
go 接收器可以用指针,也可以传值,传值的时候接收器不会改变。 如果以下两种情况,请使用指针:
- mystruct 很大时,需要拷贝的成本太高
- 方法需要修改 myStruct
Note:如果对象有可能并发执行方法,指针接收器中可能产生数据竞争,记得加锁
func(s * MyStruct)pointerMethod(){ // 指针方法
s.Age = -1 // useful
}
func(s MyStruct)valueMethod(){ // 值方法
s.Age = -1 // no use
}
for 循环里的是副本
for key, element = range aContainer {...}
- 实际遍历的 aContainer 是原始值的一个副本
- element 是遍历到的元素原始值的一个副本
- key 和 Value 整个循环都是同一个变量,每次迭代都生成新变量
aContainer和element的拷贝成本。aContainer数组的时候的拷贝成本比较大,而切片和map的拷贝成本比较小。如果想要缩小拷贝成本,我们有几个建议:
- 遍历大数组时,可以先创建大数组的切片再放在range后面
- element结构比较大的时候,直接用下标key遍历,舍弃element
map 的值不可取址
map 是哈希表的实现,所以值的地址在哈希表动态调整的时候会可能产生变化,因此,存在着 map 值的地址是没意义的,go 禁止了 map 取址操作,一下类型都不可取址
- map 元素
- string 的字节元素
- 常量(有名变量和字面量都不可以)
- 中间结果值(函数调用,显示值转换,各种操作)
// 下面这几行编译不通过。
_ = &[3]int{2, 3, 5}[0] //字面量
_ = ↦[int]bool{1: true}[1] //字面量
const pi = 3.14
_ = π //有名常量
m := map[int]bool{1: true}
_ = &m[1] //map的value
lt := [3]int{2, 3, 5}
_ = <[1:1] //切片操作
常用的仓库
strings
有 strings 库,不要重复造轮子,很多人试图再写一遍,没必要
字符串前后处理
var s = "abaay森z众xbbab"
o := fmt.Println
o(strings.TrimPrefix(s, "ab")) // aay森z众xbbab
o(strings.TrimSuffix(s, "ab")) // abaay森z众xbb
o(strings.TrimLeft(s, "ab")) // y森z众xbbab
o(strings.TrimRight(s, "ab")) // abaay森z众x
o(strings.Trim(s, "ab")) // y森z众x
o(strings.TrimFunc(s, func(r rune) bool {
return r < 128 // trim all ascii chars
})) // 森z众
字符串分割与合并
// "1 2 3" -> ["1","2","3"]
func Fields(s string) []string // 用空白字符分割字符串
// "1|2|3" -> ["1","2","3"]
func Split(s, sep string) []string // 用sep分割字符串,sep会被去掉
// ["1","2","3"] -> "1,2,3"
func Join(a []string, sep string) string // 将一系列字符串连接为一个字符串,之间用sep来分隔
// Note:
// "1||3" -> ["1","","3"]
错误处理
- 可以把异常传递下去,并不丢失自己的类型
- 可以保存堆栈信息
for range
如果每个元素比较大,循环时,使用 range 取值的方式遍历,性能比较差
package bench
import "testing"
var X [1 << 15]struct {
val int
_ [4096]byte
}
var Result int
func BenchmarkRangeIndex(b *testing.B) {
var r int
for n := 0; n < b.N; n++ {
for i := range X {
x := &X[i]
r += x.val
}
}
Result = r
}
func BenchmarkRangeValue(b *testing.B) {
var r int
for n := 0; n < b.N; n++ {
for _, x := range X {
r += x.val
}
}
Result = r
}
func BenchmarkFor(b *testing.B) {
var r int
for n := 0; n < b.N; n++ {
for i := 0; i < len(X); i++ {
x := &X[i]
r += x.val
}
}
Result = r
}
执行命令
go test -bench=. bench_test.go
欢迎关注:程序员开发者社区
学习资料
- 《Go Tour》(一个小时学会Go)
- 《The Go Programming Language Specification》(语法细节)#Introduction(中文版《Go语言编码规范》)
- 《Go语言圣经》(语法细节)
- 《Effective Go》(适合刚学完Go的基础语法时候读)
- 《Go语言设计和实现》(适合想了解Go某个特性实现原理的时候参考)
- 《Go Q&A 101》(可以和官方QA结合看)#time-sleep-after
- 《Go 语言高级编程》
- 《Go语言原本》
- 《Google Go代码规范》
- 《Uber Go规范》
- #