Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(1)https://developer.aliyun.com/article/1534255
2.1.6、切片的判空
正因为切片是引用类型的,所以切片之间不能用 == 来进行比较,而且切片的判空不能使用 s == nil 来判断,而是通过 len(s) == 0 来判断。
2.1.7、切片引用
func main(){ s1 := make([]int,2,10) // [0,0] s2 := s1 s2[0] = 1 fmt.Println(s1) // [1,0] fmt.Println(s2) // [1,0] }
可以看到,s1 把自己的内存地址赋值给了 s2,所以当 s2 对切片进行操作的时候,操作的是和 s1 共享的内存地址,所以都受影响。
func main(){ s1 := make([]int,2,10) // [0,0] fmt.Println(s1) // [1,0] addOne(s1) fmt.Println(s1) // [2,1] } func addOne(s []int){ for i:=0;i<len(s);i++{ s[i] += 1 } }
再比如这里,当切片作为参数传递进来时,函数中虽然操作的是形参,但是实参也发生了变化,这就是因为切片是一个引用类型,形参和实参指向同一内存地址。
2.1.8、append 函数为切片添加元素
Go 语言的内部函数 append() 可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
func main(){ var s1 []int s1 = append(s1,1) s1 = append(s1,2,3,4) fmt.Println(s1) // [1 2 3 4] s2 := []int{5,6,7} s1 = append(s1,s2...) fmt.Println(s1) // [1 2 3 4 5 6 7] }
2.1.9、切片扩容
切片的底层是数组,而数组的大小是固定的。这也就是为什么上面给切片添加元素时,搞那么复杂( append 函数的结果返回给原来的切片,而不是 切片.append(元素) )。
func main(){ var s1 []int fmt.Printf("size(s1)=%v,cap(s1)=%v,addr=%p \n",len(s1),cap(s1),s1) s1 = append(s1,1,2,3,4) fmt.Printf("size(s1)=%v,cap(s1)=%v,addr=%p \n",len(s1),cap(s1),s1) }
运行结果:
size(s1)=0,cap(s1)=0,addr=0x0 size(s1)=4,cap(s1)=4,addr=0xc000072020
可以看到,每扩容一次,切片的地址就会发生变化。
注意:如果是普通值类型的话,我们要取它的地址的话得配合 & 使用,但是如果是引用类型就不需要!因为引用类型本身存储的就是内存地址,不需要再用取地址符取。
Go语言中引用类型和值类型的处理方式与内存分配有关:
- 引用类型(如切片、映射、通道、指针以及函数等)的变量存储的是实际数据的引用(或称为指针),即它们存储的是数据所在的内存地址。当你传递一个引用类型的变量给一个函数时,实际上是传递了该变量所引用的数据的地址,因此函数内部对该地址指向的数据进行修改会影响到原始数据。这就是为什么你不需要使用 & 来获取引用类型变量的地址。
- 值类型(如整型、浮点型、布尔型、字符串以及数组等)的变量直接存储了实际的数据值。当你传递一个值类型的变量给一个函数时,会创建该变量的一个副本并将其传递给函数。这意味着函数内部对副本进行的修改不会影响到原始数据。如果你想让函数能够修改原始数据,你需要传递该变量的地址,这就需要使用 & 来获取值类型变量的地址。
2.1.10、使用 copy 函数覆盖切片
我们创建一个切片,希望两个切片的元素一致。如果直接赋值给另一个切片时,这个两个切片将指向同一个内存地址,一个修改另一个也会被修改。
所以,我们可以通过 copy 函数来进行切片的复制:
func main(){ s1 := []int{1,2,3} s2 := []int{4,5,6} copy(s2,s1) fmt.Println(s1) //[1,2,3] fmt.Println(s2) //[1,2,3] s2[0] = -1 fmt.Println(s1) //[1,2,3] fmt.Println(s2) //[-1,2,3] }
2.1.11、删除切片中某个元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素:
// 删除索引为 2 的元素 func main(){ s1 := []int{0,1,2,3,4,5} s1 = append(s1[:2],s1[3:]...) fmt.Println(s1) // [0,1,3,4,5] }
这样,我们对 append 函数有了更深刻的认识,append(p1,p2) 的意思是:在切片 p1 的基础上添加p2(p2可以是切片也可以是单个或多个元素) 中的元素。
3、指针
指针:指针是一种数据类型,用于存储一个内存地址,该地址指向存储在该内存中的对象。
区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
3.1、指针地址和指针类型
声明指针变量(指针变量的值是地址):
a := 10 // 声明一个指针变量b,它的值为a的地址 b := &a
上面我们声明了一个指针变量 b,它的指针类型为 *int 。
a := 10 b := &a fmt.Printf("a=%d addr=%p \n",a,&a) // a=10 addr=0xc000014028 fmt.Printf("addr=%p type=%T \n",b,b) // addr=0xc000014028 type=*int fmt.Println(&b) // 0xc00000e030
通过上面的结果可以看到,因为 b 存储了 a 的地址,所以 b 的值和 a 的地址是一样的,而 b 也有自己的地址。
3.2、指针取值
a := 10 b := &a fmt.Printf("type of b: %T \n",b) // type of b: *int c := *b fmt.Printf("type of c: %T \n",c) // type of c: int fmt.Printf("value of c = %v ",c) // value of c = 10
可以看到,变量 c 是数值类型,它把指针变量 b 的值( a 的内存地址)对应的值了取出来。
总结: 取地址操作符 & 和取值操作符 * 是一对互补操作符,& 取出地址,* 根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
3.3、指针传值
之前我们知道值类型的变量传递给函数后无法被操作,因为函数中的形参和实参的地址是不同的。所以,学了指针之后,我们可以通过给函数传递指针地址来保证形参和实参操作的是同一个地址对应的值。
func main(){ a := 10 fmt.Println(a) // 10 addOne(&a) fmt.Println(a) // 11 } func addOne(num *int){ *num += 1 }
3.4、new 和 make
var b map[string]int b["李大喜"] = 22 fmt.Println(b)
对于上面的代码,运行会直接异常:panic: assignment to entry in nil map。这是因为 b 是引用类型(map),我们还没有给它分配内存空间就直接使用了。而值类型是不需要分配内存,因为我们在声明的时候会有默认值。
而要分配内存,就引出来 Go 语言中的new和make,它俩都是 Go 内建的两个函数,主要用来分配内存。
3.4.1、new
new 函数在源码中是这样的:
func new(Type) *Type
- Type 代表类型,new 函数的参数是一个类型,而一般的函数参数是值。
- *Type 代表类型指针,new 函数返回一个指向该类型的内存地址的指针。
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的初始值:
func main(){ // 声明一个int类型指针 该指针并没有初始值,因为指针的值是其它变量的内存地址 var a *int // 为指针a开辟内存空间,默认指向0的地址值 a = new(int) // 取出指针的值 fmt.Println(*a) // 0 *a = 10 fmt.Println(*a) // 10 }
3.4.2、make
make 也是用于内存分配的,区别于new,它只用于slice、map以及channel的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的源码:
func make(t Type, size ...IntegerType) Type
- t Type:表示传入一个 Type 类型的变量 t
- size... IntegerType:表示传入一个或多个整型的值
我们之前在动态创建切片(相对的是通过数组创建切片)的时候就是使用的 make。make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。
var score map[string]int score = make(map[string]int,10) score["小明"] = 98 fmt.Println(len(score)) // 1 fmt.Println(score) // map[小明:98]
这里,我们通过 make 函数为 map 开辟了 10 个内存空间,并使用了一个内存空间。
3.4.3、new 和 make 的区别
- 二者都是用来做内存分配的;
- make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
- 而new用于类型的内存分配,并且内存对应的值为类型初始值,返回的是指向类型的指针;
4、map
4.1、 map 定义
map 的定义:
map[KeyType]ValueType
map类型的变量默认初始值为 nil,需要使用make()函数来分配内存:
make(map[KeyType]ValueType, [cap])
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
比如:
users := make(map[string]int)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(3)https://developer.aliyun.com/article/1534260