go 高级编程中一些注意点
-
数组、字符串和切片三者是密切相关的数据结构,因底层都是相同的结构。
- go中除了闭包函数以引用的方式对外部变量访问之外,其它赋值和函数传参数都是以传值的方式处理。
- 数组,需要指定固定长度,特殊类型,值类型,元素可修改,赋值和函数调用实质是以整体复制处理。不同长度或不同类型的数据组成的数组都是不同的类型,所以无法直接赋值。数组len和cap返回的数据是一样的。指针指向数组,和数组值,都可以用下标和range访问数组的值。
var a [ 3 ] int // 定义一个长度为3的int类 型数组, 元素全部为0 var b = [...] int { 1 , 2 , 3 } // 定义一个长度为3的 int类型数组, 元素为 1, 2, 3 var c = [...] int { 2 : 3 , 1 : 2 } // 定义一个长度为3 的int类型数组, 元素为 0, 2, 3 var d = [...] int { 1 , 2 , 4 : 5 , 6 } // 定义一个长度为 6的int类型数组, 元素为 1, 2, 0, 0, 5, 6
特殊:长度为0的数组在内存不会占用空间,可以用于强调某种特有类型的操作时避免分配额外的内存空间,比如用于管道的同步操作,chan上无需传输数据。
c1 := make ( chan [ 0 ] int ) go func () { fmt.Println( "c1" ) c1 <- [ 0 ] int {} }()
<- c1// 必须用key来初始化结构体,原因:结构体初始化有两种:1.指定字段名称。2.按顺序列出所有字段,不指定名称。如果使用2的方式进行初始化,假如别人重新定义了这个结构,中间加了字段,就会出现初始化不对的问题。protobuf也是这样做的。 type NoUnkeyedLiterals struct{} // 不允许结构体比较,只能针对到 ==,!=,>=,<=,>,<;如果使用反射relect.DeepEqual还是可以比较的。原理对于struct,只有全部字段是compareable才可以比较Slice、Map、Function均是不可比较的,只能判断是否为nil type DoNotCompare [0]func() // 不允许结构体拷贝、值传递,本质是 WaitGroup 内部维护了计数,不允许 copy 变量,内嵌 noCopy 字段,防止 Cond 变量被复制,还有sync.Mutex锁也是不允许copy的。 type DoNotCopy [0]sync.Mutex type User struct { Age int Address string //放在定义尾部更能节省空间padding // 必须用key来初始化结构体 NoUnkeyedLiterals // 不允许结构体比较 DoNotCompare // 不允许结构体拷贝、值传递 DoNotCopy } func main() { _ = &User{Age: 21, Address: "beijing"} fmt.Println(unsafe.Sizeof(NoUnkeyedLiterals{})) fmt.Printf("%p\n", &DoNotCopy{}) fmt.Printf("%p\n", &NoUnkeyedLiterals{}) fmt.Printf("%p\n", &DoNotCompare{}) }
package main import ( "sync" ) func test(wg sync.WaitGroup) { defer wg.Done() wg.Add(1) } // 经典WaitGroup变量死锁 func main() { var wg sync.WaitGroup wg.Add(1) go test(wg) wg.Wait() }
拓展:与零长度类似,空结构体也是可以用来节省内存,具体场景有:
- 将map作为Set使用时,可以将值类型定义为空结构体,仅作为占位符使用;
- channel不需要发送数据,只用来通知子协程执行任务等时,可使用空结构体作为占位符;
- 结构体只包含方法,不包含任何的字段时,可声明空结构体,以节省资源。
- 字符串,底层为byte数组,只读禁止了对底层字节数组的修改。字符串赋值只是复制了数据地址和对应的长度,而不会导致底层数据的复制。赋值操作也就是 reflect.StringHeader 结构体的复制过程,reflect.StringHeader包含两个类型。
type StringHeader struct { Data uintptr Len int }
- 字符串是支持转为[]rune和[]byte,rune是int32的别称。
字符串相关的强制类型转换主要涉及到 []byte 和 []rune 两 种类型。每个转换都可能隐含重新分配内存的代价,最坏的情 况下它们的运算时间复杂度都是 O(n)。因为[]byte 和 []int32 类型是完全不同的内部布局,所以并不知道一个字符串的底层到底是什么数组实现,就会存在重新分配内存的问题。
- 切片,切片要注意的是,在容量不足的情况下, append 的操作会导 致重新分配内存,可能导致巨大的内存分配和复制数据代价。但是因为这个特性,想避免切片出现问题的时候,可以用append(空切片, 元素)来创建一个新的切片,避免出现原切片增加删除,但后面一直有个指针引用了原数据导致不能及时gc的问题。
- append也可以往前加入元素,这就要指定具体的下标。在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次,所以性能不高。
var a = [] int { 1 , 2 , 3 } a = append ([] int { 0 }, a...) // 在开头添加1个元素 a = append ([] int { -3 , -2 , -1 }, a...) // 在开头添加1个切片
-
中间插入1个、或批量 元素
a = append (a, 0 ) // 切片扩展1个空间 copy (a[i+ 1 :], a[i:]) // a[i:]向后移动1个位置 a[i] = x // 设置新添加的元素
a = append (a, x...) // 为x切片扩展足够的空间 copy (a[i+ len (x):], a[i:]) // a[i:]向后移动len(x)个位置 copy (a[i:], x) // 复制新添加的切片
普通链式操作实现中间插入元素
var a [] int
a = append (a[:i], append ([] int {x}, a[i:]...)...) // 在第i个位置插入x a = append (a[:i], append ([] int { 1 , 2 , 3 }, a[i:]...)...) // 在第i个位置插入切片 - 删除元素
a = [] int { 1 , 2 , 3 } a = a[: len (a) -1 ] // 删除尾部1个元素 a = a[: len (a)-N] // 删除尾部N个元素 // 删除开头的元素也可以不移动数据指针,但是将后面的数据向开头移动。 //可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存 //区间内完成,不会导致内存空间结构的变化) a = [] int { 1 , 2 , 3 } a = append (a[: 0 ], a[ 1 :]...) // 删除开头1个元素 a = append (a[: 0 ], a[N:]...) // 删除开头N个元素 //用copy删除 a = [] int { 1 , 2 , 3 } a = a[: copy (a, a[ 1 :])] // 删除开头1个元素 a = a[: copy (a, a[N:])] // 删除开头N个元素 //删除中间 a = [] int { 1 , 2 , 3 , ...} a = append (a[:i], a[i+ 1 :]...) // 删除中间1个元素 a = append (a[:i], a[i+N:]...) // 删除中间N个元素 a = a[:i+ copy (a[i:], a[i+ 1 :])] // 删除中间1个元素 a = a[:i+ copy (a[i:], a[i+N:])] // 删除中间N个元素 //0长切片实现删除字符串的空格 func TrimSpace(s [] byte ) [] byte { b := s[: 0 ] for _, x := range s { if x != ' ' { b = append (b, x) } } return b } //其实类似的根据过滤条件原地删除切片元素的算法都可以采用 类似的方式处理(因为是删除操作不会出现内存不足的情形): func Filter(s [] byte , fn func (x byte ) bool ) [] byte { b := s[: 0 ] for _, x := range s { if !fn(x) { b = append (b, x) } } return b }
- 切片高效操作的要点是要降低内存分配的次数,尽量保证 append 操作不会超出 cap 的容量,降低触发内存分配的次数和每次分配内存大小。
-
避免切片内存泄漏切片操作并不会复制底层的数据。底层的数组会 被保存在内存中,直到它不再被引用。但是有时候可能会因为 一个小的内存引用而导致底层整个数组处于被使用的状态,这 会延迟自动内存回收器对底层数组的回收。这段代码返回的 []byte 指向保存整个文件的数组。切片b引用了整个原始数组,其次返回的正则匹配返回的byte[]数组引用b的部分,导致自动垃圾回收器不能及时释放底层数组的空间。
//比如这个就会泄漏 func FindPhoneNumber(filename string ) [] byte { b, _ := ioutil.ReadFile(filename) return regexp.MustCompile( "[0-9]+" ).Find(b) }
解决方案,用空切片特征,复制到一个新切片里
func FindPhoneNumber(filename string ) [] byte { b, _ := ioutil.ReadFile(filename) b = regexp.MustCompile( "[0-9]+" ).Find(b) return append ([] byte {}, b...) }
类似的删除也会出现
//类似的问题,在删除切片元素时可能会遇到。假设切片里存放 的是指针对象,那么下面删除末尾的元素后,被删除的元素依 然被切片底层数组引用,从而导致不能及时被自动垃圾回收器 回收(这要依赖回收器的实现方式) var a []* int { ... } a = a[: len (a) -1 ] // 被删除的最后一个元素依然被引用, 可能导 致GC操作被阻碍 //保险的方式是先将需要自动内存回收的元素设置为 nil ,保 证自动回收器可以发现需要回收的对象,然后再进行切片的删除操作 var a []* int { ... } a[ len (a) -1 ] = nil // GC回收最后一个元素内存 a = a[: len (a) -1 ] // 从切片删除最后一个元素
- append也可以往前加入元素,这就要指定具体的下标。在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次,所以性能不高。
- go中函数,闭包函数对捕获的外部变量并不是传值方式访问,而是以引用的方式访问。 闭包的这种引用方式访问外部变量的行为可能会导致一些隐含的问题:
//因为是闭包,在 for 迭代语句中,每个 defer 语句延迟执 行的函数引用的都是同一个 i 迭代变量,在循环结束后这个变量的值为3,因此最终输出的都是3。
//可以通过加上参数i
func main() { for i := 0; i < 3; i++ { defer func() { println(i) }() } }第一种方法是在循环体内部defer前再定义一个局部变量 i:=i,这样每次迭代 defer 语句的闭包函数捕获的都是不同的变量,这些变量的值对应迭代时的值。第二种方式是将迭代变量通过闭包函数 的参数传入defer func (i int ){ println (i) } (i)
, defer 语句会马上对调用参数求值。两种方式 都是可以工作的。 - Go语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为Go语言运行时会根据需要动态地调整函数栈的大小。但函数栈是使用连续动态栈,
当连续栈动态增长时,需要将之前的数据移动到新的内存空间,这会导致之前栈中全部变量的地址发生变化。虽然Go语言运行时会自动更新引用了地址变化的栈变量的指针,但最重要的一点是要 明白Go语言中指针不再是固定不变的了(因此不能随意将指针保持到数值变量中,Go语言的地址也不能随意保存到不在GC控制的环境中,因此使用CGO时不能在C语言中长期持有 Go语言对象的地址)。
-
在Go语言规范中甚至故意没有讲到栈和堆的概念。我们无法知道函数参数或局部变量到底是保存在栈中还是堆中,我们只需要知道它们能够正常工作就可以了。
package main import "fmt" func main() { var a = 1 fmt.Println(&a) b := f(a) fmt.Println(b) } func f(x int) *int { fmt.Println(&x) return &x } // 代码打印的地址是一样的,说明函数的参数在这个地方,是放在堆上的,不然main拿不回。go是会智能的把变量创建在堆或者栈上
- 单例的实现
func main() { a := Instance("hi") fmt.Fprintf(os.Stdout, "%v", a) } var ( instance *singleton once sync.Once ) type singleton struct { name string } func Instance(name string) *singleton { once.Do(func() { instance = &singleton{name} }) return instance }
-
chan的使用,chan丢数据进去,如果想要被消费后 才往下走 那就需要无缓冲的chan,阻塞。如果想丢进去,直接返回,那就是要有缓冲的chan,但是缓冲满了一样阻塞
package main import ( "fmt" "strings" "sync" "time" ) type ( subscriber chan interface{} // 订阅者为一个管道 topicFunc func(v interface{}) bool // 主题为一个过滤器 ) type Publisher struct { m sync.RWMutex // 读写锁 buffer int // 订阅队列的缓存大小 timeout time.Duration // 发布超时时间 subscribers map[subscriber]topicFunc // 订阅者信息 } // 构建一个发布者对象, 可以设置发布超时时间和缓存队列的长度 func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher { return &Publisher{buffer: buffer, timeout: publishTimeout, subscribers: map[subscriber]topicFunc{}} } // 添加一个新的订阅者,订阅全部主题 func (p *Publisher) Subscribe() chan interface{} { return p.SubscribeTopic(nil) } // 添加一个新的订阅者,订阅过滤器筛选后的主题 func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} { ch := make(chan interface{}, p.buffer) p.m.Lock() p.subscribers[ch] = topic p.m.Unlock() return ch } // 退出订阅 func (p *Publisher) Evict(sub chan interface{}) { p.m.Lock() defer p.m.Unlock() delete(p.subscribers, sub) close(sub) } // 发布一个主题 func (p *Publisher) Publish(v interface{}) { p.m.Lock() defer p.m.Unlock() var wg sync.WaitGroup for sub, topic := range p.subscribers { wg.Add(1) go p.sendTopic(sub, topic, v, &wg) } } // 发送主题,可以容忍一定的超时 func (p *Publisher) sendTopic(sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup) { defer wg.Done() // topic为nil 直接下一步select就是发给所有了;不为nil,调用订阅者的过滤函数,返回true,才下一步select if topic != nil && !topic(v) { return } select { // 把v信息,发给了订阅者sub case sub <- v: { fmt.Printf("v:%v receive by %v \n", v, sub) } // 订阅者接受超时了就case到timeout不阻塞 case <-time.After(p.timeout): { fmt.Printf("timeout %v %v \n", sub, v) } } } // 关闭发布者对象,同时关闭所有的订阅者管道。 func (p *Publisher) Close() { p.m.Lock() defer p.m.Unlock() for sub := range p.subscribers { delete(p.subscribers, sub) close(sub) } } // 有两个订阅者分别订阅了全部主题和含有"golang"的主题 func main() { p := NewPublisher(100*time.Millisecond, 0) defer p.Close() // 添加一个订阅者,订阅了全部主题 all := p.Subscribe() // 添加一个订阅者,过滤golang字符串,也就是只订阅了golang golang := p.SubscribeTopic(func(v interface{}) bool { // 接口类型强转string,看是否成功,成功就调用contains,过滤 if s, ok := v.(string); ok { return strings.Contains(s, "golang") } return false }) p.Publish("hello, world!") p.Publish("hello, golang!") go func() { for msg := range all { fmt.Println("all:", msg) } }() go func() { for msg := range golang { fmt.Println("golang:", msg) } }() // 运行一定时间后退出 time.Sleep(3 * time.Second) }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!