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

14.go深入:非类型安全unsafe

unsafe是不安全的,可以绕过go的安全检查机制,直接对内存进行读写,非必要不使用。 指针类型转换 go是一门强类型的静态语言,意味着一旦定义了,类型就不能改变,且类型检查在运行前已经完成。 出于安全考虑,go不允许两个指针类型进行转换,比如 *int 不能转换成 *float。 i := 10 ip := &i var fp *float64 = (*float64)(ip) // 尝试将 *int 转换成 *float64 在编译的时候,会报错: cannot convert ip (type * int) to type * float64,提示不可转换 unsafe.Pointer unsafe.Pointer表示任意类型的指针,上述例子中,我们使用 unsafe.Pointer进行中转,实现指针类型转换 i := 10 ip := &i var fp *float64 = (*float64)(unsafe.Pointer(ip)) // 尝试将 *int 转换成 *float64 *fp = *fp * 3 fmt.Println(i,*fp, ip, fp) 输出: 30 1.5e-322 0xc0000aa058 0xc0000aa058 可以看到,通过转化后的指针修改指向内存的值,原始值i也变成了30,ip和fp指向的是同一块内存地址。需要注意的是,试图通过 *fp 打印出原来内存的数据时,却出现了一个异常值,说明指针可以通过 unsafe.Pointer 进行转化,但转化后的指针不一定可以访问到原始内存的值。 unsafe.Pointer源码定义: // ArbitraryType is here for the purposes of documentation // only and is not actually part of the unsafe package. // It represents the type of an arbitrary Go expression. type ArbitraryType int type Pointer *ArbitraryType ArbitraryType可以表示任意类型,在源码中只是文档需要。unsafe.Pointer是 *ArbitraryType,表示通用型指针,可以表示任何内存地址。 ...

<span title='2022-03-15 22:26:20 +0800 +0800'>March 15, 2022</span>&nbsp;·&nbsp;2 min&nbsp;·&nbsp;LingZihuan

13.go深入:reflect 运行时反射

啥是反射 go语言中,反射为我们提供了一种可以在运行时操作任意类型对象的能力,比如,查看一个接口变量的具体类型、看一个结构体有多少字段、修改某个字段的值等。 比如 fmt.Println: func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) } 函数定义中有一个可变参数 a ...interface{},我们在调用的时候,可以传1个到多个参数进去。 reflect.Value 和 reflect.Type go语言的反射定义中,任何接口都有两个部分组成:接口的具体类型,以及具体类型对应的值。如 var i in = 3,由于 interface{}可以表示任何类型,因此i可以转化为 interface{},将其当做一个接口,此时它在go反射中就表示成 <Value, Type>,其中Value为3,Type为int。 go反射中,标准库为我们提供了两种类型 reflect.Value和 reflect.Type分别表示变量的值和类型,并且可以用函数 reflect.ValueOf和 reflect.TypeOf分别获取任意对象 Value和Type。 func main(){ var i int = 3 iv := reflect.ValueOf(i) it := reflect.TypeOf(i) fmt.Println(iv, it) } reflect.Value 结构体定义 reflect.Value 可以通过 reflect.ValueOf获得,其结构体定义如下 type Value struct { typ *rtype ptr unsafe.Pointer flag } 其里面的变量都是私有的,意味着我们只能使用它的方法,它的常用方法有: //针对具体类型的系列方法 //以下是用于获取对应的值 Bool Bytes Complex Float Int String Uint CanSet //是否可以修改对应的值 // 以下是用于修改对应的值 Set SetBool SetBytes SetComplex SetFloat SetInt SetString Elem //获取指针指向的值,一般用于修改对应的值 //以下Field系列方法用于获取struct类型中的字段 Field FieldByIndex FieldByName FieldByNameFunc Interface //获取对应的原始类型 IsNil //值是否为nil IsZero //值是否是零值 Kind //获取对应的类型类别,比如Array、Slice、Map等 //获取对应的方法 Method MethodByName NumField //获取struct类型中字段的数量 NumMethod//类型上方法集的数量 Type//获取对应的reflect.Type 可以总结为3类: ...

<span title='2022-03-14 00:33:13 +0800 +0800'>March 14, 2022</span>&nbsp;·&nbsp;5 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

11. go深入:值、指针以及引用类型

值类型 假设我们有一个这样的结构体: type person struct { name string age uint } 然后我们试图定义一个函数,修改这个结构体实例的值: func modify(argP person){ argP.name = "Nobody" argP.age = 10 } // 我们实际运行一下这个函数 func main(){ p := person{name: "mike", age: 19} fmt.Println(p) modify(p) fmt.Println(p) } 上述的例子中,我们期望在经过了modify函数修改后,打印出来的结果是 {Nobody, 10},然而实际上,输出的仍然是初始化的值: {mike, 19} 为啥会这样呢,因为上面定义的变量 p 是值类型,而通过modify函数传入的变量值argP只是原始值变量p的一份值拷贝,而不是原来的数据本身。 其实,我们只要在modify函数内部和外部打印出变量p和argP的地址,就可以发现他们的不同之处。 func modify(argP person){ fmt.Printf("address of arg p: %p\n", &argP) // ... } func main(){ // ... fmt.Printf("address of p: %p\n", &p) modify(p) // ... } 输出为: address of p: 0xc0002905a0 address of arg p: 0xc0000d01b0 可以看到,在函数内部和外部,变量的地址是不一样的。 实际上,在go语言中,函数的传参都是值传递,也就是原来数据的一份拷贝,而不是数据本身。以上述的modify函数来说,参数argP就是原始变量p的一份值拷贝。除了struct之外,还有浮点型、整形、字符串、布尔、数组等,这些都是值类型 指针类型 回到最初的期望,我们想要修改 person 的值,只需要修改modify函数的参数为指针类型(*person),然后在对应的函数体中修改原始数据的值: func modify(argP *person) { fmt.Printf("address of arg p: %p\n", argP) argP.name = "Nobody" argP.age = 10 } 这样,就Ok了。 ...

<span title='2022-03-10 14:44:08 +0800 +0800'>March 10, 2022</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;LingZihuan