背景

每一门开发语言的基础都是从数据类型开始学起,Java转成Golang,所以小编的学习之路又从零开始了。Golang和其他开发语言一样分为数据类型分为两种值类型和引用类型,值类型比较简单就是一些基本数据类型,无论是否有过其他语言基础,大概看一下也是可以明白的,所以本文主要介绍Golang的引用类型。

基础

值类型:变量直接存储值,内容通常在栈中分配

引用类型:变量存储的是一个地址,这个地址存储最终的值,内容通常在堆上分配,通过GC回收

从Golang的设计思想上来说,Golang是函数式编程,函数式编程最明显的特点不可变,所以Golang的传参特点值传递,默认传递是值,那怎么处理引用传递呢?主要利用本文讲解的这几种引用类型。

Golang中引用类型:指针、slice(切片)、map、chan,chan和并发编程联系比较紧密,放到后面的并发编程中,主要讲解指针、slice、map

数组

 1. 数组:是同一种数据类型的固定长度的序列。
2. 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
3. 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。
4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
for i := 0; i < len(a); i++ {
}
for index, v := range a {
}
5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
6. 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
7.支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
8.指针数组 [n]*T,数组指针 *[n]T。

数组初始化方式

package main

import (
"fmt"
)
//全局
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}

func main() {
//局部
a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4: 200} // 使用引号初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素类型。
{"user2", 20}, // 别忘了最后一行的逗号。
}
//打印全局
fmt.Println(arr0, arr1, arr2, str)
//打印局部
fmt.Println(a, b, c, d)
}

注意点

1、数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。

package main

import (
"fmt"
)

func test(x [2]int) {
fmt.Printf("x: %p\n", &x)
x[1] = 1000
}

func main() {
a := [2]int{}
fmt.Printf("a: %p\n", &a)

test(a)
fmt.Println(a)
}

//打印结果
a: 0xc42007c010
x: 0xc42007c030
[0 0]

通过这段代码可以推出数组是值类型,在传输的过程中都是复制,内存地址已经不同。

值拷贝行为会造成性能问题,如何解决这个问题,则引入slice,和指针

slice(切片)

slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

1. 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
2. 切片的长度可以改变,因此,切片是一个可变的数组。
3. 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。
4. cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
5. 切片的定义:var 变量名 []类型,比如 var str []string var arr []int。
6. 如果 slice == nil,那么 len、cap 结果都等于 0。

创建切片的方式

//1.声明切片
var s1 []int
if s1 == nil {
fmt.Println("是空")
} else {
fmt.Println("不是空")
}
// 2.:=
s2 := []int{}
// 3.make()
var s3 []int = make([]int, 0)

//make创建具体参数明显
var slice []type = make([]type, len)
slice := make([]type, len)
slice := make([]type, len, cap)

s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。
fmt.Println(s1, len(s1), cap(s1))

s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
fmt.Println(s2, len(s2), cap(s2))

s3 := make([]int, 6) // 省略 cap,相当于 cap = len。

make创建切片的内存分配

【Golang基础篇】——array、slice、指针、map_开发语言

 切片初始化

全局:
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[start:end]
var slice1 []int = arr[:end]
var slice2 []int = arr[start:]
var slice3 []int = arr[:]
var slice4 = arr[:len(arr)-1] //去掉切片的最后一个元素
局部:
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr[start:end]
slice6 := arr[:end]
slice7 := arr[start:]
slice8 := arr[:]
slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素

【Golang基础篇】——array、slice、指针、map_引用类型_02

数组和切片的内存布局

【Golang基础篇】——array、slice、指针、map_后端_03

使用 make 动态创建slice,避免了数组必须用常量做长度的麻烦。还可用指针直接访问底层数组,退化成普通数组操作。

package main

import "fmt"

func main() {
s := []int{0, 1, 2, 3}
p := &s[2] // *int, 获取底层数组元素指针。
*p += 100

fmt.Println(s)
}

输出结果:

[0 1 102 3]

用append内置函数操作切片

append :向 slice 尾部添加数据,返回新的 slice 对象。 

package main

import (
"fmt"
)

func main() {

s1 := make([]int, 0, 5)
fmt.Printf("%p\n", &s1)

s2 := append(s1, 1)
fmt.Printf("%p\n", &s2)

fmt.Println(s1, s2)

}

输出结果:

0xc42000a060
0xc42000a080
[] [1]

超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满

package main

import (
"fmt"
)

func main() {

data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3]

s = append(s, 100, 200) // 一次 append 两个值,超出 s.cap 限制。

fmt.Println(s, data) // 重新分配底层数组,与原数组无关。
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。

}
输出结果
[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
0xc4200160f0 0xc420070060

指针

Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。

​&​​*​
​(int、float、bool、string、array、struct)​​*int、*int64、*string​

变量指针

ptr := &v    // v的类型为T
v:代表被取地址的变量,类型为T
ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。

从内存角度看下区别

func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
}

【Golang基础篇】——array、slice、指针、map_引用类型_04

可以看到b的实际存储的是a的变量的内存地址,所以b被称为指针类型,那么如何获取指针类型真正的数据值呢?即指针取值

func main() {
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}

type of b:*int
type of c:int
value of c:10
​*​​&​​*​

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

    1.对变量进行取地址(&)操作,可以获得这个变量的指针变量。
2.指针变量的值是指针地址。
3.对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

空指针是所有程序员逃不开的困难,Go中何为空指针

  • 当一个指针被定义后没有分配到任何变量时,它的值为 nil
  • 空指针的判断,判断是为nil即可

new和make的使用

new是一个内置的函数,它的函数签名如下:
func new(Type) *Type

1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了

func make(t Type, size ...IntegerType) Type

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

var b map[string]int // 只是声明变量b是一个map类型的变量 此时b=nil
b = make(map[string]int, 10) //需要make进行初始化后才能使用,否会panic

new和make的区别

  1.二者都是用来做内存分配的。
2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

map

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

//Go中map定义 
map[KeyType]ValueType

KeyType:表示键的类型。
ValueType:表示键对应的值的类型。

map类型的变量默认初始值为nil,需要使用make()函数来分配内存
make(map[KeyType]ValueType, [cap])
cap是make的容量,非必填,但是我们应该在初始化map的时候就为其指定一个合适的容量

map的基本使用

func main() {
//填充
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
//在声明时填充
userInfo := map[string]string{
"username": "pprof.cn",
"password": "123456",
}

//判断key是否存在,定义 value, ok := map[key]
// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}

//map遍历 range
//同时需要k,v
for k, v := range scoreMap {
fmt.Println(k, v)
}
//只需要k
for k := range scoreMap {
fmt.Println(k)
}

//删除指定key、delete(map, key)
//map:表示要删除键值对的map
//key:表示要删除的键值对的键
delete(scoreMap, "小明")//将小明:100从map中删除

}

总结

array是值类型,但是是引用类型扩展的基石,值类型的拷贝会存在性能问题,所以Go中提供了引用类型,引用类型的使用离不开指针,搞懂指针需要类型,指针地址、指针类型、指针取值即可,&获取地址,*根据地址取出地址指向的值。slice相当于提供了动态可自由扩容的数组,而map则相当于提供了可根据指定值查找value的数组。从简化的角度上来看还是相对简单的,如果有其他语言基础,举一反三效果会更好些。当然在使用它们的过程,还是有些坑点,后续的文章会继续更新slice、map底层实现机制,以及特殊注意点。

PS:Go系列的第一篇终于开始了,Java转Go的我,终于要认真的转Go了,初学者,如有理解错误支持,恳请各位大佬赐教,不胜感激。