引用数据类型
指针
获取变量的地址,用&,比如var num int,获取num的地址:&num
var a int = 10
fmt.Println("a 的地址=" + &a)
指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值,比如:var ptr int = &num
获取指针类型所指向的值,使用:,比如,var ptr int,使用ptr获取ptr指向的值
var a int = 10 //变量
var ptr *int = &a //指针变量
fmt.Printf("ptr 的地址=%v \n", &ptr)
fmt.Printf("ptr的值是 a 的地址=%v \n", &a)
fmt.Printf("ptr 所指向地址的值=%v \n", *ptr)
值类型,都有对应的指针类型,形式为 *数据类型, 值类型包括:基本数据类型、数组和结构体struct。
var a int = 10 //变量
var ptr *int = &a //指针变量
fmt.Printf("ptr 的地址=%v \n", &ptr)
var b float64 = .123
var piont *float64 = &b
fmt.Printf("piont 的地址=%v \n", &piont)
值类型与引用类型
区分
- 值类型:基本数据类型(int系列、float系列、bool、string)、数组和结构体
- 引用类型:指针、slice切片、map、管道chan、interface等都是引用类型
使用特点
- 值类型:变量直接存储值,内存通常在栈中分配
- 引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量应用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收。
数组
数组可以存放多个同一类型数据。数组也是一种数据类型,在 Go 中,数组是值类型。
- 定义与赋值
var 数组名 [数组大小]数据类型
var intArr [3]int //int占8个字节
//当我们定义完数组后,其实数组的各个元素有默认值 0
//赋值
intArr[0] = 10
intArr[1] = 20
intArr[2] = 30
//四种初始化数组的方式
var numArr01 [3]int = [3]int{1, 2, 3}
var numArr02 = [3]int{5, 6, 7}
var numArr03 = [...]int{8, 9, 10} //这里的 [...] 是规定的写法,不确定大小
var numArr04 = [...]int{1: 800, 0: 900, 2:999} //下标赋值
//类型推导
strArr05 := [...]string{1: "tom", 0: "jack", 2:"mary"}
- 数组在内存布局
- 数组的地址可以通过数组名来获取 &intArr
- 数组的第一个元素的地址,就是数组的首地址
- 数组的各个元素的地址间隔是依据数组的类型决定,比如 int64 -> 8 int32->4.
- 数组遍历
- 常规 for循环
- for-range结构遍历:这是 Go 语言一种独有的结构,可以用来遍历访问数组的元素。
heroes := [...]string{"宋江", "吴用", "卢俊义"}
for index, value := range heroes {
fmt.Printf("index=%v value=%v\n", index , value)
fmt.Printf("heroes[%d]=%v\n", index, heroes[index])
}
for _, v := range heroes {
fmt.Printf("元素的值=%v\n", v)
}
1. 第一个返回值 index是数组的下标
2. 第二个value是在该下标位置的值
3. 他们都是仅在 for循环内部可见的局部变量
4. 遍历数组元素的时 候,如果不想使用下标index,可以直接把下标index标为下划线_
5. index和value的名称不是固定的,即程序员可以自行指定.一般命名为index和value
- 注意事项
- 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化
- var arr []int 声明一个数组没有定义长度,arr 就是一个 slice 切片
- 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用。
- 数组创建后,如果没有赋值,有默认值(零值)
数值类型数组:默认值为 0
字符串数组:默认值为 ""
bool 数组: 默认值为 false
- 使用数组的步骤 1. 声明数组并开辟空间 2 给数组各个元素赋值(默认零值) 3 使用数组
- 数组的下标是从 0 开始的
- 数组下标必须在指定范围内使用,否则报 panic:数组越界
- Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响
- 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
- 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度
多维数组
二维数组
- 使用方式
- 先声明/定义,再赋值
语法: var 数组名 [大小][大小]类型
//定义/声明二维数组
var arr [2][3]int
//赋初值
arr[1][2] = 1
arr[2][1] = 2
arr[2][3] = 3
- 直接初始化
var 数组名 [大小][大小]类型 = [大小][大小]类型{
{初值..},{初值..}}
二维数组在声明/定义时也对应有四种写法[和一维数组类似]
var 数组名 [大小][大小]类型 = [大小][大小]类型{
{初值..},{初值..}}
var 数组名 [大小][大小]类型 = [...][大小]类型{
{初值..},{初值..}}
var 数组名 = [大小][大小]类型{
{初值..},{初值..}}
var 数组名 = [...][大小]类型{
{初值..},{初值..}}
- 二维数组在内存的存在形式
var arr [2][3]int //以这个为例来分析arr2在内存的布局!!
arr[1][1] = 10
fmt.Println(arr) //[[0 0 0] [0 10 0]]
fmt.Printf("arr[0]的地址%p\n", &arr[0]) //arr[0]的地址0xc000018090
fmt.Printf("arr[1]的地址%p\n", &arr[1]) //arr[1]的地址0xc0000180a8
//0xc000018090和 0xc0000180a8 相差 3x8: 3个int元素 (1个int 8byte)
fmt.Printf("arr[0][0]的地址%p\n", &arr[0][0]) //arr[0][0]的地址0xc000018090
//&arr2[0] == &arr[0][0]
fmt.Printf("arr[1][0]的地址%p\n", &arr[1][0]) //arr[1][0]的地址0xc0000180a8
&arr[1] == &arr[1][0]
总结
1. 二维数组内存形式存储的是指针
2. 二维数组第一组存储的第一组第一个元素的地址,第二组存储的是第二组第一个元素的地址,依次类推
3. 二维数组两组地址相差的是一组元素所占的字节
- 二维数组的遍历
- 双层 for 循环完成遍历
var arr = [2][3]int{
{1,2,3}, {4,5,6}}
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[i]); j++ {
fmt.Printf("%v\t", arr[i][j])
}
}
- for-range 方式完成遍历
var arr = [2][3]int{
{1,2,3}, {4,5,6}}
for i, v := range arr {
for j, v2 := range v {
fmt.Printf("arr[%v][%v]=%v \t",i, j, v2)
}
}
切片
- 定义
- 切片的英文是 slice
- 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
- 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。
- 切片的长度是可以变化的,因此切片是一个可以动态变化数组。
- 切片定义的基本语法:
//var 切片名 []类型
var a [] int
- 切片的内存形式
- slice 的确是一个引用类型
- slice 从底层来说,其实就是一个数据结构(struct 结构体)
type slice struct {
ptr *[2]int //截取数组开始位置的地址
len int //截取的长度
cap //容量
}
- 切片的使用
- 定义一个切片,然后让切片去引用一个已经创建好的数组
var slice = arr[startIndex:endIndex]
说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])。
var slice = arr[0:end] 可以简写 var slice = arr[:end]
var slice = arr[start:len(arr)] 可以简写: var slice = arr[start:]
var slice = arr[0:len(arr)] 可以简写: var slice = arr[:]
- 通过 make 来创建切片.
基本语法:var 切片名 []type = make([]type, len, [cap])
参数说明: type: 就是数据类型 len : 大小 cap :指定切片容量,可选(如果你分配了 cap, 则 cap>=len)
1. 通过 make 方式创建切片可以指定切片的大小和容量
2. 如果没有给切片的各个元素赋值,那么就会使用默认值[int , float=> 0 string =>"" bool =>false]
3. 通过 make 方式创建的切片对应的数组是由 make 底层维护,对外不可见,即只能通过 slice 去访问各个元素.
- 定义一个切片,直接就指定具体数组,使用原理类似 make 的方式
var strSlice []string = []string{"tom", "jack", "mary"}
方式 1 和方式 2 的区别
方式1是直接引用数组,这个数组是事先存在的,程序员是可见的。
方式2是通过make未创建切片,make也会创建一个数组,是由切片在底层进行维护,程序员是看不见的。
- 切片的遍历
- for 循环常规方式遍历
var arr [5]int = [...]int{10, 20, 30, 40, 50}
slice := arr[1:4]
for i := 0; i < len(slice); i++ {
fmt.Printf("slice[%v]=%v ", i, slice[i])
}
- for-range 结构遍历切片
var arr [5]int = [...]int{10, 20, 30, 40, 50}
slice := arr[1:4]
for i, v := range slice {
fmt.Printf("i=%v v=%v \n", i, v)
}
- 注意事项
- 切片初始化时 var slice = arr[startIndex:endIndex]
说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])。 - 切片初始化时,仍然不能越界。范围在 [0-len(arr)] 之间,但是可以动态增长.
- cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素。
- 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make一个空间供切片来使用
- 切片可以继续切片
- 用 append 内置函数,可以对切片进行动态追加
var slice []int = []int{100, 200, 300}
//通过append直接给slice3追加具体的元素
slice = append(slice, 400, 500, 600)
//通过append将切片slice追加给slice
slice = append(slice, slice...)
切片 append 操作的底层原理分析:
1. 切片 append 操作的本质就是对数组扩容
2. go 底层会创建一下新的数组 newArr(安装扩容后大小)
3. 将 slice 原来包含的元素拷贝到新的数组 newArr
4. slice 重新引用到 newArr
5. 注意 newArr 是在底层来维护的,程序员不可见.
- 切片的拷贝:切片使用 copy 内置函数完成拷贝
var slice1 []int = []int{1, 2, 3, 4, 5}
var slice2 = make([]int, 10)
copy(slice2, slice1)
fmt.Println("slice1=", slice1)// 1, 2, 3, 4, 5
fmt.Println("slice2=", slice2) // 1, 2, 3, 4, 5, 0 , 0 ,0,0,0
对上面代码的说明:
1. copy(para1, para2) 参数的数据类型是切片
2. 按照上面的代码来看, slice4 和 slice5 的数据空间是独立,相互不影响,也就是说 slice4[0]= 999,slice5[0] 仍然是 1
- 切片是引用类型,所以在传递时,遵守引用传递机制。
var arr [5]int = [...]int{10, 20, 30, 40, 50}
slice1 := arr[1:4] // 20, 30, 40
slice2 := slice1[1:2] //[30]
slice2[0] = 100 // 因为arr , slice1 和slice2 指向的数据空间是同一个,因此slice2[0]=100,其它的都变化
- string 和 slice
- string 底层是一个 byte 数组,因此 string 也可以进行切片处理
- string 的内存形式
type slice struct {
ptr *[4]byte //截取数组开始位置的地址
len int //截取的长度
}
- string 是不可变的,也就说不能通过 str[0] = ‘z’ 方式来修改字符串
- 如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string
str := "hello@world"
arr1 := []byte(str)
arr1[0] = 'z'
str = string(arr1)
//我们转成[]byte后,可以处理英文和数字,但是不能处理中文
// 原因是 []byte 字节来处理 ,而一个汉字,是3个字节,因此就会出现乱码
// 解决方法是 将 string 转成 []rune 即可, 因为 []rune是按字符处理,兼容汉字
arr1 := []rune(str)
arr1[0] = '北'
str = string(arr1)
map
map 是 key-value 数据结构,又称为字段或者关联数组。类似其它编程语言的集合
- 基本语法
var 变量名 map[keytype]valuetype
keytype:
golang 中的 map,的 key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只包含前面几个类型的 接口, 结构体, 数组 通常 key 为 int 、string
注意: slice, map, function 不可以,因为这几个没法用 == 来判断
valuetype:
valuetype 的类型和 key 基本一样
- 声明和使用
- 声明:
var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]map[string]string
注意:声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用。
-
使用:
方式一:var a map[string]string //在使用map前,需要先make , make的作用就是给map分配数据空间 a = make(map[string]string, 10) a["no1"] = "宋江" a["no2"] = "吴用" a["no1"] = "武松" a["no3"] = "吴用"
方式二:
cities := make(map[string]string) cities["no1"] = "北京" cities["no2"] = "天津" cities["no3"] = "上海"
方式三:
heroes := map[string]string{ "hero1" : "宋江", "hero2" : "卢俊义", "hero3" : "吴用", }
- map 的增删改查操作
cities := make(map[string]string)
//增
cities["no1"] = "北京" //如果 key 还没有,就是增加,如果 key 存在就是修改。
cities["no2"] = "天津"
cities["no3"] = "上海"
//删
delete(cities, "no1")
//当delete指定的key不存在时,删除不会操作,也不会报错
delete(cities, "no4")
//改
//因为 no3这个key已经存在,因此下面的这句话就是修改
cities["no3"] = "西安"
//查
val, ok := cities["no2"]
if ok {
fmt.Printf("有no1 key 值为%v\n", val)
} else {
fmt.Printf("没有no1 key\n")
}
//如果希望一次性删除所有的key
//1. 遍历所有的key,逐一删除 [遍历]
//2. 直接make一个新的空间
cities = make(map[string]string)
- map 遍历
只能使用for-range遍历
cities := make(map[string]string)
cities["no1"] = "北京"
cities["no2"] = "天津"
cities["no3"] = "上海"
for k, v := range cities {
fmt.Printf("k=%v v=%v\n", k, v)
}
- map 切片
//1. 声明一个map切片
var monsters []map[string]string
monsters = make([]map[string]string, 2)
//2. 增加第一个妖怪的信息
if monsters[0] == nil {
monsters[0] = make(map[string]string, 2)
monsters[0]["name"] = "牛魔王"
monsters[0]["age"] = "500"
}
//3. 切片的append函数,可以动态的增加monster
newMonster := map[string]string{
"name" : "新的妖怪~火云邪神",
"age" : "200",
}
monsters = append(monsters, newMonster)
- map 排序
map1 := make(map[int]int, 10)
map1[10] = 100
map1[1] = 13
map1[4] = 56
map1[8] = 90
//如果按照map的key的顺序进行排序输出
//1. 先将map的key 放入到 切片中
var keys []int
for k, _ := range map1 {
keys = append(keys, k)
}
//2. 对切片排序
sort.Ints(keys)
fmt.Println(keys)
//3. 遍历切片,然后按照key来输出map的值
for _, k := range keys{
fmt.Printf("map1[%v]=%v \n", k, map1[k])
}
- 使用细节
- map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来
- map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长 键值对(key-value)
- map 的 value 也经常使用 struct 类型,更适合管理复杂的数据(比前面 value 是一个 map 更好),
结构体
- Golang 语言面向对象编程说明
- Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
- Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。
- Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等
- Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。
- Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang 中面向接口编程是非常重要的特性。
- 结构体和结构体变量(实例)的区别和联系
- 结构体是自定义的数据类型,代表一类事物.
type Cat struct {
Name string
Age int
Color string
Hobby string
Scores [3]int
}
- 结构体变量(实例)是具体的,实际的,代表一个具体变量
var cat1 Cat
cat1.Name = "小白"
cat1.Age = 3
cat1.Color = "白色"
cat1.Hobby = "吃<・)))><<"
- 结构体变量(实例)在内存的布局
var cat1 Cat // var a int
cat1.Name = "小白"
cat1.Age = 3
cat1.Color = "白色"
cat1.Hobby = "吃<・)))><<"
fmt.Printf("cat1的地址=%p\n", &cat1)
fmt.Printf("cat1.Name的地址=%p\n", &cat1.Name)
fmt.Printf("cat1.Age的地址=%p\n", &cat1.Age)
fmt.Printf("cat1.Color的地址=%p\n", &cat1.Color)
fmt.Printf("cat1.Hobby的地址=%p\n", &cat1.Hobby)
结果:
cat1的地址=0xc00007e000 //824634236928
cat1.Name的地址=0xc00007e000 //824634236928 //string占位 16byte
cat1.Age的地址=0xc00007e010 //824634236944 //int占位 8byte
cat1.Color的地址=0xc00007e018 //824634236952
cat1.Hobby的地址=0xc00007e028 //824634236968
总结:
&cat1 == &cat1.Name
&cat1.Name + 16 = &cat1.Age
&cat1.Age + 8 = &cat1.Color
- 声明结构体
基本语法
type 结构体名称 struct {
字段1 type //结构体字段 = 属性 = field
字段2 type
}
举例:
type Student struct { //结构体和字段名大写代表public,小写表示private
Name string
Age int
Score float32
}
字段 :字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。
字段细节说明
1) 字段声明语法同变量,示例:字段名 字段类型
2) 字段的类型可以为:基本类型、数组或引用类型
3) 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:
布尔类型是 false ,数值是 0 ,字符串是 ""。
数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]
指针,slice,和 map 的零值都是 nil ,即还没有分配空间。
4) 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。
- 创建结构体变量和访问结构体字段
- 直接声明
var person Person
var person Person = Person{}
举例:
p2 := Person{"mary", 20}
var person *Person = new (Person)
(*p3).Name = "smith" //(*p3).Name = "smith" 也可以这样写 p3.Name = "smith"
/*
* 原因: go的设计者 为了程序员使用方便,底层会对 p3.Name = "smith" 进行处理
* 会给 p3 加上 取值运算 (*p3).Name = "smith"
*/
var person *Person = &Person{}
//var person *Person = &Person{"mary", 60}
(*person).Name = "scott" //person.Name = "scott"
(*person).Age = 88 //person.Age = 88
- 注意事项
- 结构体的所有字段在内存中是连续的
假如有两个 Point类型,这个两个Point类型的本身地址也是连续的,但是他们指向的地址不一定是连续 - 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
- 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
- struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
package main
import "fmt"
import "encoding/json"
type Monster struct{
Name string `json:"name"` // `json:"name"` 就是 struct tag
Age int `json:"age"`
Skill string `json:"skill"`
}
func main() {
//1. 创建一个Monster变量
monster := Monster{"牛魔王", 500, "芭蕉扇~"}
//2. 将monster变量序列化为 json格式字串
// json.Marshal 函数中使用反射,这个讲解反射时,我会详细介绍
jsonStr, err := json.Marshal(monster)
if err != nil {
fmt.Println("json 处理错误 ", err)
}
fmt.Println("jsonStr", string(jsonStr))
}
方法
Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct。
- 声明
func (recevier type) methodName(参数列表) (返回值列表){
方法体
return 返回值
}
说明:
1. 参数列表:表示数据类型调用传递给方法的参数
2. recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
3. receiver type : type 可以是结构体,也可以其它的自定义类型
4. receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
5. 返回值列表:表示返回的值,可以多个
6. 方法主体:表示为了实现某一功能代码块
7. return 语句不是必须的。
- 调用
type A struct {
Num int
}
func (a A) test() {
fmt.Println(a.Num)
}
func main() {
var a A
a.test() //调用方法
}
说明
1. func (a A) test() {} 表示 A 结构体有一方法,方法名为 test
2. (a A) 体现 test 方法是和 A 类型绑定的
3. test 方法只能通过 A 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
4. func (a A) test() {}... a 表示哪个 A 变量调用,这个 a 就是它的副本, 这点和函数传参非常相似。
- 方法的调用和传参机制原理
方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)。 - 注意事项
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
- 如希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
- Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
- 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出
方法和函数区别
- 调用方式不一样
函数的调用方式: 函数名(实参列表)
方法的调用方式: 变量.方法名(实参列表) - 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
- 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
type Person struct {
Name string
}
//函数
func test01(p Person) {
fmt.Println(p.Name)
}
func test02(p *Person) {
fmt.Println(p.Name)
}
//方法
func (p Person) test03() {
p.Name = "jack"
fmt.Println("test03() =", p.Name) // jack
}
func (p *Person) test04() {
p.Name = "mary"
fmt.Println("test03() =", p.Name) // mary
}
func main() {
p := Person{"tom"}
test01(p)
test02(&p)
p.test03()
fmt.Println("main() p.name=", p.Name) // tom
(&p).test03() // 从形式上是传入地址,但是本质仍然是值拷贝
fmt.Println("main() p.name=", p.Name) // tom
(&p).test04()
fmt.Println("main() p.name=", p.Name) // mary
p.test04() // 等价 (&p).test04 , 从形式上是传入值类型,但是本质仍然是地址拷贝
fmt.Println("main() p.name=", p.Name) // mary
}
总结:
1. 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
2. 如果是和值类型,比如(p Person) , 则是值拷贝; 如果和指针类型,比如是 (p *Person) 则是地址拷贝。