15.go深入:SliceHeader,高效的slice

数组 数组由两部分组成:数组的大小和数组内部的元素类型。 // 伪代码表示 array { len item type } 看以下两个数组的定义: a1 := [1]string{"mike"} a2 := [2]string{"mike"} 上述定义的两个变量,a1的类型为 [1]string,a2的类型为 [2]string,所以说,数组的大小也属于数组类型的一部分。 数组的两个限制 **一旦一个数组被声明,它的大小和内部的类型就不能改变,**我们将不能随意向其中追加任意多的元素。 另外,当我们使用数组存储大量数据,然后将数组作为函数的参数进行传值时,由于函数之间是值传递的,因此,数组的拷贝将会耗费巨大的内存。 slice 切片 我们可以将切片理解为动态的数组。 切片是对数组的封装,它的底层是一个数组存储了所有的元素,但是它可以动态地添加元素,容量不足时可以自动扩容。 动态扩容 使用内置的append方法,向切片中追加元素,返回一个新的切片。 同时,当容量不足的时候,append会自动对切片进行扩容。 func main(){ ss := []string{"mike"} // 定义切片ss fmt.Printf("slice before append: %s, length=%d, cap=%d\n", ss, len(ss), cap(ss)) ss = append(ss, "lucy", "john") // append 追加元素 fmt.Printf("slice after append: %s, length=%d, cap=%d\n", ss, len(ss), cap(ss)) } 输出: slice before append: [mike], length=1, cap=1 slice after append: [mike lucy john], length=3, cap=3 数据结构 切片在go语言中是一个数据结构: type SliceHeader struct { Data uintptr // 指向存储切片元素的数组 Len int // 切片长度 Cap int // 切片容量 } 示例证明: ...

<span title='2022-03-18 14:34:12 +0800 +0800'>March 18, 2022</span>&nbsp;·&nbsp;3 min&nbsp;·&nbsp;LingZihuan

12.go深入:new、make和内存分配

Go语言程序所管理的虚拟内存空间会被分为两部分:堆内存和栈内存。栈内存主要由Go语言来管理,开发者无法干涉太多,堆内存则有开发者进行分配。 变量 一个数据类型,在声明之后,会被赋值给一个变量,变量存储了程序所需的数据。 变量的声明 单纯的声明变量,可以使用var关键字,如: var s string // 声明一个[字符串]变量,初始值为零值"" var sp *string // 声明一个[字符串指针]变量,初始值为 nil 变量的初始化 有3种方法 声明的时候直接初始化: var name string = "mike" 声明之后再进行赋值初始化: name = "mike",此前 name 变量已经声明 直接使用 := 进行初始化: name := "mike" 值变量和指针变量初始化的区别 我们使用值初始化的时候,可以简单的这样写: var name string name = "mike" 但是,当我们使用指针初始化的时候: var nameP *string // 声明一个字符串指针 *nameP = "mike" // 给nameP指向的地址赋值初始化 这时候,由于 nameP 指向的是一个空地址 nil,我们对这个空地址进行赋值初始化的时候,会报以下错误: panic: runtime error: invalid memory address or nil pointer dereference 显而易见,我们无法对一个空地址赋值。 因此,**如果要对一个变量赋值,这个变量必须有对应的分配好的内存,这样才可以对这块内存操作,完成赋值的目的。**不止是赋值操作,针对指针变量,如果没有分配内存,对其进行取值时一样会报 nil异常,因为没有可以操作的内存。 new函数 声明指针变量的时候,是默认没有分配内存的,因此,我们可以使用 new 函数来进行内存的分配: var nameP *string nameP = new(string) // 分配一块string类型的内存 *nameP := "mike" fmt.Println(*nameP) 这样,我们的nameP变量指向的就不是一个 nil 空地址了,而是指向了一块具体的字符串的内存。因此可以对其进行赋值操作。 ...

<span title='2022-03-13 23:35:38 +0800 +0800'>March 13, 2022</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;LingZihuan

10. go深入:Go 中的指针

什么是指针 可以简单将指针理解为内存地址。 指针是一种数据类型,用来存储一个内存地址,该地址指向存储在该内存中的对象,可以是整型、字符串、或者是我们自定义的任意结构体类型。 也可以理解为:指针就是一本书上的目录上面的页码,这个页码指向具体的内容。 指针的声明和定义 以字符串指针为例 // 声明一个字符串指针变量 var sp *string // 直接通过 & 操作符获取一个字符串的地址 s := "Hello world." sp := &s // 还可以使用 new 函数,传入一个类型作为参数,用以返回该类型的指针 sp := new(string) 指针的操作 针对指针的操作,有两种:获取或者修改指针指向的值 获取指针指向的值 使用 *指针变量 获取指针指向的值,如 *sp 获取sp指针指向的内存地址的值。 修改指针指向的值 修改跟获取值也是类似的,使用 *指针变量 = 值 来修改,如 *sp = "new value",这样,就把 sp指针指向的内存地址值修改为了 new value 注意 另外,通过 var sp *string 定义的sp指针,初始值是nil,表示它没有指向任何一块内存地址,我们不能够对它进行取值和赋值操作,否则,会提示: invalid memory address or nil pointer dereference 要解决这个问题,只需要使用 new 函数给该地址分配一块内存即可: var sp *string = new(string) ...

<span title='2022-03-06 00:00:00 +0000 UTC'>March 6, 2022</span>&nbsp;·&nbsp;2 min&nbsp;·&nbsp;LingZihuan

9. go并发:Go 中的并发模式

for select 模式 这是一种常见的并发模式,我们一般使用 for {} 死循环,然后里面加select配合channel,获取协程的终止信号,来控制协程的退出 for { select { case <- done: // received stop signal, stopping. return default: // processing ... } } 类似的还有 for range select模式,主要就是使用 for ... range 遍历某个数据数组,将数据发送到channel里面: for _, value := range []int{} { select { case <- done: // 接受到终止信号 return case resultCh <- value: // do nothing but put value to rsult channel } } select timeout 模式 select timeout模式,核心在于使用 time.After(t time.Duration) 返回一个定时的channel,使用select配合这个channel控制协程的退出。 来看一个网络访问超时的demo: func main(){ result := make(chan string) go func(){ // simulate internet visiting... time.Sleep(5 * time.Second) result <- "Done, internet visited." }() select { case v := <-result: fmt.Println("result from internet: ", v) case <- time.After(3 * time.Second): fmt.Println("Timeout!") } } pipeline 模式 pipeline流水线模式,顾名思义,就是整一个生产流程中有多个工序,后面的工序依赖前面一道工序的结果。 ...

<span title='2022-03-04 00:00:00 +0000 UTC'>March 4, 2022</span>&nbsp;·&nbsp;3 min&nbsp;·&nbsp;LingZihuan

8. go并发:Context-多线程并发控制神器

协程如何退出 一般来说,我们执行协程,需要等到协程执行完毕,才能够退出。但是,当我们想要让协程提前退出,就需要一种机制,去控制协程的退出。 以下例子使用select + channel的方式,控制协程的退出 func main() { ch := make(chan bool) var wg sync.WaitGroup go func() { defer wg.Done() // 开启looper协程 looper(ch) }() wg.Add(1) // 5秒后发送中断信号 time.Sleep(time.Second * 5) fmt.Println("Signal to goroutine exit...") ch <- true fmt.Println("Signal sent.") wg.Wait() // 等待协程完全退出 fmt.Println("Exit.") } func looper(ch <-chan bool){ for { select { case <-ch: fmt.Println("go break signal") return default: fmt.Println("looper is running...") time.Sleep(time.Second * 1) } } } 上述例子通过一个ch channel 发送信号,让协程收到信号后终止循环。 使用Context控制协程退出 上述,我们可以通过select+ channel 的方式进行控制协程的退出,但是那是单个协程的退出,当我们想要控制多个协程的退出时,也可以用定义多个channel的方法,但是这会让代码变得十分复杂。 而Context,可以让我们跟踪到每个协程,从而更好地进行限制和控制。 使用 Context 改造上述的代码: func main() { var wg sync.WaitGroup ctx, stop := context.WithCancel(context.Background()) go func() { defer wg.Done() looper(ctx) }() wg.Add(1) time.Sleep(time.Second * 5) fmt.Println("Signal to goroutine exit...") stop() // 向context发送停止指令 fmt.Println("Signal sent.") wg.Wait() fmt.Println("Exit.") } func looper(ctx context.Context){ for { select { case <-ctx.Done(): fmt.Println("go break signal") return default: fmt.Println("looper is running...") time.Sleep(time.Second * 1) } } } 上述代码实现的功能和使用select + channel也是一样的,主要改动为: ...

<span title='2022-02-27 00:00:00 +0000 UTC'>February 27, 2022</span>&nbsp;·&nbsp;2 min&nbsp;·&nbsp;LingZihuan