Jochen的golang小抄-基础篇-章一
本系列写的是学习过程中的纯代码笔记记录,该系列为代码流,基本只写代码,代码开始前会有一段导读注释,建议先看注释在学习和练习代码
小抄系列主要分为皮毛、基础和进阶篇,本篇为基础,篇幅较长,故分为多个章节,本章主要讲golang中的复杂数据类型的内容
目录
goto语句
在上一篇中有写到程序的流程控制方面的知识点,有时候我们想不讲武德,不想遵循流程走代码,而是随心所欲,放飞自我,让代码指哪走哪。我们就可以使用goto语句,但是遍地是goto语句的代码就像是一张被重复使用无数次的草稿纸,难以阅读不说,造成程序逻辑混乱对于日后的维护必然要付出代价。所以,慎用!
package main
import "fmt"
func main() {
/*
goto语句:可以无条件让代码运行过程中跳转到指定的行
语法结构:
goto label;
...
label: statement;
*/
var i = 10
LOOP:
for i < 20 {
if i == 15 {
i += 1
fmt.Println("我跳!")
goto LOOP //15会+1跳出循环到标签LOOP位置,然后又接着往下执行程序
}
fmt.Printf("i的值为%d\n", i)
i++
}
}
随机数的生成
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
/*
随机数的生成random:
世界上没有随机数,所有的随机数都是根据一定的算法公式算出来的
什么?随机数的算法是怎么实现的?别鸟了,用了再说...
go语言中随机数的实现在:math/rand包下
很多同学在运行随机数函数的时候发现,怎么我每次运行得到的结果都一样?
随机数是由一个种子数产生的,其数据类型要求为整数,如不设置种子数则使用默认的种子数,
但由于随机数是生成而来的,所以种子数相同,生成的随机数就会相同,如果每次运行想生成不同
的随机数,可以通过Seed函数来重新设置种子
*/
i1 := rand.Int()
fmt.Println(i1) //5577006791947779410 我执行多少遍都是这玩意
//也可以指定取值范围,使用Intn(num) 其取值区间为[0,num)
i2 := rand.Intn(10)
fmt.Println(i2) //7
//设置种子数,使数值每次生成都产生变化
rand.Seed(1) //如果去掉,i3和i2就都是7
i3 := rand.Intn(10)
fmt.Println(i3) //1
//所以要生成真正的伪随机数,就需要去动态的改变种子数,什么是动态改变的呢?时间!
//时间的获取可以使用time包
t1 := time.Now() //2021-01-19 00:20:12.676036273 +0800 CST m=+0.000131128
fmt.Println(t1)
//种子数要求是整数类型,此时我们可以获取时间戳(指定时间距离1970-1-1 00:00:00之间的时间差值:秒,那秒)
timeStamp1 := t1.Unix() //秒
fmt.Println(timeStamp1) //1610986969
timeStamp2 := t1.UnixNano() //纳秒
fmt.Println(timeStamp2) //1610987037460065277
//正确使用伪随机的方式:
//step1:设置种子数,可以设置成时间戳
rand.Seed(time.Now().Unix())
println("-----------------------------------")
for i := 0; i < 10; i++ {
//step2:调用生成随机数的函数
fmt.Printf("伪随机数:%d\n", rand.Intn(100)) //[0,100)
}
//Intn(n) 获取的返回为[0,n)范围的随机数,如何获取[10,100]的随机数?->转化为[0,91)+10
i4 := rand.Intn(91) + 10
fmt.Println(i4)
//最后,别误导了,上面一直使用Intn获取整型的随机数 其实还有生成浮点类型的随机数,以及各种范围整型和浮点类型的数据数:
//rand.Float64()
//rand.Float32()
//rand.Int31()
//rand.Int31n(1)
//rand.Uint32()
//更多就去查标准库吧
}
复合数据类型
go语言中数据类型分为两类
- 基本数据类型:整数,浮点,布尔,字符串
- 复合类型:array,slice,map,struct,pointer,function(没错,go语言函数也是数据类型,因为其支持函数式编程),channel...
基本类型在皮毛篇已经全部介绍完毕,本小节主要记录复合数据类型的知识点
数组
牛刀小试
package main
import "fmt"
func main() {
/*
数组:
1.概念:存储一组相同数据类型的数据结构
理解为容器,存储一组数据
可以通过索引来读取或者修改数组中的元素,索引是从0开始的,即
第一个元素索引为0,第二个为1,以此类推,所以数组的下标取值范围是 -> [0,数组长度-1]
2.语法:下面见名知意
var variable_name [Size] variable_type
var variable_name = [Size]variable_type{元素1,元素2...}
variable_name := [...]variable_type{元素1,元素2...}
3.通过下标访问
下标,也叫索引:index
默认从0开始的整数,直到长度减1
数组名[index]
赋值
取值
4.长度和容量:go语言的内置函数
len(array/map/slice/string),长度
cap(),容量
注意:数组一旦定义后,大小不能更改
*/
//一个变量只能存储一个数值
var num1 int = 100
println(num1)
//当想要使用一个变量统一管理一组数,则需要使用数组
//step1:创建数组
var arr1 [4]int
//setp2:数组的访问
arr1[0] = 1
arr1[1] = 2
arr1[2] = 3
for i := 0; i < 4; i++ {
println(arr1[i]) //遍历打印数组的每一个元素
//可以看到打印数组的最后一个未赋值的元素为0,因为int的零值(默认值)为0
}
//下标不能超过数组的边界
//fmt.Println(arr1[4]) //invalid array index 4 (out of bounds for 4-element array)
//获取数组的长度和容量
//因为数组是定长,长度和容量相同
fmt.Println("数组的长度", len(arr1)) //3 长度是容器中实际存储的数据量
fmt.Println("数组的容量", cap(arr1)) //4 容量是容器能存储的最大数据量
//可以通过下标改变数组中的元素
arr1[0] = 88
println(arr1[0]) //1->88
//上面一个个通过下标赋值数组的方式确实有点low,可以使用数组的其他创建方式,下面创建方式可以初始化数组
var arr2 = [4]int{1, 2, 3} //与var arr2[4] int相同
fmt.Println(arr2) //[1 2 3 0]
//可以指定初始化元素的下标
var arr3 = [3]int{2: 3, 1: 1, 0: 8}
//println(arr3) //illegal types for operand: print 内置的打印函数不可以打印数组类型
fmt.Println(arr3) //[8 1 3]
//初始化数组{}中的元素个数不能大于[]中的数组
//var arr4 = [3]int{1,2,3,4} //array index 3 out of bounds [0:3]
//fmt.Println(arr4)
//字符串的零值为空字符串
var ss = [3]string{"jochen", "demon"}
fmt.Println(ss) //[jochen demon ] <-右括号明显多个空行,表示空字符串
//其他创建方式
is := [3]int{} //等价于 var is[3] int
fmt.Println(is) //[0 0 0]
fs := [...]float64{0.1, 0.2, 0.3} //自动推断元素个数创建数组
fmt.Println(fs) //[0.1 0.2 0.3]
//自动推断还可以通过下标初始化元素个数去推断元素的容量
var is2 = [...]int{8: 1, 10: 2, 3: 1} //最大使用下标是10所以数组容量为10
fmt.Println(is2) //[0 0 0 1 0 0 0 0 1 0 2]
}
数组的内存分析
package main
import "fmt"
func main() {
/*
数组内存分析:
- 创建变量实际就是开辟内存,我们只关心变量的数据类型,存储的数值和变量的名字,
通过变量名字可以访问内存空间去进行赋值和取值
- 数据中元素的内存地址是连续的,每个元素所占用的内存大小相同
*/
//基本数据类型的变量开辟的内存地址是随机的分配无序的
var num1 = 1
fmt.Printf("数组的内存地址为%p\n", &num1) //数组的内存地址为0xc0000b6010
var num2 = 2
fmt.Printf("数组的内存地址为%p\n", &num2) //数组的内存地址为0xc00001c0e0
//数据中每一个元素的内存地址是连续的,可以看到int类型数组每一个元素占用八个字节存储空间,它们内存地址是连续的
//注意下面内存地址的数值为16进制数
var arr = [...]int{1, 2, 3}
fmt.Printf("数组的内存地址为%p\n", &arr[0]) //数组的内存地址为0xc0000c0020
fmt.Printf("数组的内存地址为%p\n", &arr[1]) //数组的内存地址为0xc000016468
fmt.Printf("数组的内存地址为%p\n", &arr[2]) //数组的内存地址为0xc000016470
}
数组的遍历方式
package main
import "fmt"
func main() {
/*
数组的遍历:挨个访问数组中的每一个元素
var arr = [...]int{1,2,3,4,5,6}
方式一(low):arr[0],arr[1],arr[2]...
方法二:通过循环,配合下标
方式三(强烈推荐):使用range
不需要操作数组下标,到达数组的末尾,自动结束for range循环
每次都从数组中获取下标对应的值
*/
var arr = [...]int{1, 2, 3, 4, 5, 6}
//方法一,遗忘掉吧不介绍,实在不想写
//fmt.Printf("%v\t", arr[0])
//fmt.Printf("%v\t", arr[1])
//方式二:for循环变量搭配数据长度内置函数
for i := 0; i < len(arr); i++ {
fmt.Printf("%v\t", arr[i])
} //1 2 3 4 5 6
println("\n----------------------------")
//方式三:使用range遍历数组,妙蛙种子方式,你就说妙不妙
for i, v := range arr {
fmt.Printf("我是下标:%v,我是值%v\n", i, v)
}
/*
方式三执行结果结果:
我是下标:0,我是值1
我是下标:1,我是值2
我是下标:2,我是值3
我是下标:3,我是值4
我是下标:4,我是值5
我是下标:5,我是值6
*/
println("----------------------------")
//如果只需要值并希望忽略索引,可以通过使用_blank标识符替换索引来实现
sum := 0
for _, v := range arr {
sum += v
}
fmt.Println(sum)
}
数组是值类型
package main
import "fmt"
func main() {
/*
go语言中数据类型分为两类
- 基本数据类型:整数,浮点,布尔,字符串
- 复合类型:array,slice,map,struct,pointer,function,channel...
数组的数据类型:
[size]type
golang中的数组不同于别的语言,数组是值类型
值类型:理解为存储的数值本身
将数据传递给其他的变量,传递的是数据的副本(备份)
int,float,string,bool,array
引用类型:理解为存储数的内存地址
sclice,map...
*/
//1.数据类型
num := 1
fmt.Printf("%T\n", num) //int
//数组的数据类型
arr1 := [...]int{1, 2, 3}
arr2 := [...]float64{0.1, 0.2, 0.3}
arr3 := [...]string{"Jochen", "Tom", "Demon"}
fmt.Printf("arr1类型为:%T,值为:%v\n", arr1, arr1) //arr1类型为:[3]int,值为:[1 2 3]
fmt.Printf("arr2类型为:%T,值为:%v\n", arr2, arr2) //arr2类型为:[3]float64,值为:[0.1 0.2 0.3]
fmt.Printf("arr3类型为:%T,值为:%v\n", arr3, arr3) //arr3类型为:[3]string,值为:[Jochen Tom Demon]
//2.赋值
num2 := num //值传递
fmt.Println(num, num2) //1 1
num2 = 2 //改变num2观察num1是否改变
fmt.Println(num, num2) //1 2 可以看到对num2赋值不会影响num1
//所以num赋值给num2是把自己的值复制了一份给了num2,改变num2不会对num1产生影响,反之亦然这就是值传递
//观察数组现象
arr4 := arr1
fmt.Printf("arr1:%v, arr4:%v\n", arr1, arr4) //arr1:[1 2 3], arr4:[1 2 3]
arr4[0] = 888 //把arr4中的一个元素的值改变
fmt.Printf("arr1:%v, arr4:%v\n", arr1, arr4) //arr1:[1 2 3], arr4:[888 2 3]
//可以看到arr4的改变不会影响arr1,索引arr1赋值给arr4的实际上也是自身的一个副本,而非引用,所以数组也是值传递
//3.数组的比较
a := 1
b := 4
fmt.Println(a == b) //比较a和b的数值是否相等
//上面将arr4的第一个元素改了值
fmt.Println(arr4 == arr1) //false
//改回去
arr4[0] = 1
fmt.Println(arr4 == arr1) //true
//所以数组的比较也是比较数组上的各个元素的数组是否相等,比较的两个数组要求长度,类型一致
//fmt.Println(arr1 == arr2) //invalid operation: arr1 == arr2 (mismatched types [3]int and [3]float64)
}
练习:数组的排序
package main
import "fmt"
func main() {
/*
数组的排序
让数组的元素具有一定的顺序
*/
//冒泡排序
arr := [5]int{16, 100, 7, 99, 3}
//第一轮排序
for j := 0; j < 4; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j] //[3 7 16 99 100]
}
}
fmt.Println(arr) //[15 8 10 7 23] 最大的元素冒泡到最后
//第二轮
for j := 0; j < 3; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j] //[7 16 3 99 100]
}
}
fmt.Println(arr) //[8 10 7 15 23]
//...以此类推发现,每轮排序代码相同,只是j的取值每次都-1,所以我们可以使用嵌套循环
//代码细品吧,就是找规律
for i := 1; i < len(arr); i++ {
for j := 0; j < len(arr)-i; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j] //[3 7 16 99 100]
}
}
}
fmt.Println(arr) //[7 8 10 15 23]
}
多维数组
package main
import "fmt"
func main() {
/*
Go支持多维数组
一维数组:存储的多个数据是数值本身
a1 := [2]int{1, 2}
二维数组:存储的是一维的一维,即一维数组里面的元素是一维指定类型数组
a2 := [2][3]int{{}, {}, {}}
多维数组:可以俄罗斯套娃,多维可以是多层套娃嵌套数组
a3 := [3][2][1]int{{{1}, {2}}, {{3}, {4}}, {{5}, {6}}}
*/
//二维数组
arr2 := [2][3]int{{1, 2, 3}, {6, 7, 8}} //[[1 2 3] [6 7 8]]
fmt.Println(arr2)
fmt.Printf("二维数组的地址:%p\n", &arr2) //:0xc000018180
fmt.Printf("二维数组的长度:%d\n", len(arr2)) //2 就是里面一维数组的个数
fmt.Println("第一个一维数组的值:", arr2[0]) //[1 2 3]
fmt.Printf("第一个一维数组的地址:%p\n", &arr2[0]) //:0xc000018180
fmt.Printf("第一个一维数组的长度:%d\n", len(arr2[0])) //3 一维数组的元素个数
//遍历二维数组
for i, arr := range arr2 {
for j, num := range arr {
fmt.Printf("a[%d][%d]=%d\t", i, j, num)
}
println()
}
/*
输出:两行三列的矩阵
a[0][0]=1 a[0][1]=2 a[0][2]=3
a[1][0]=6 a[1][1]=7 a[1][2]=8
*/
//套娃大法
arr3 := [1][2][3]int{{{1, 2, 3}, {3, 2, 1}}}
fmt.Println(arr3) //[[[1 2 3] [3 2 1]]]
}
切片(slice)
切片(Slice):变长数组
- 理解:切片是对数组的抽象,数组的长度是定长不可变的,在特定场景就不太适用
切片实际上就是动态数组,其长度不固定,可追加元素
切片本身不存储任何数据,其只是对现有数组的引用 - 特点:变长
- 本质:是一个“引用类型”(数组是值类型)容器,指向了一个顶层数组
slice更像是一个结构体,这个结构体中包含了三个元素:- 指针:数组中slice指定的开始位置
- 长度,数组是定长所以长度和容量是相同的,slice中长度和容量的概念才能体现出来
- 最大长度,即数组中容量的概念
牛刀小试
package main
import "fmt"
func main() {
/*
切片(Slice):变长数组
理解:切片是对数组的抽象,数组的长度是定长不可变的,在特定场景就不太适用
定义:
var slice1 []type
切片不需要说明长度
初始化:
s :=[] int {1,2,3}
使用make()函数创建切片:
func make(t Type, size ...IntegerType) Type //...表示可变参数,表示可以传多个值
第一个参数:类型(引用类型)
slice,map,chanl
第二参数:长度len
实际存储存储元素的数量
第三个参数:容量cap
最多能够存储的元素的数量
例子:
slice2 := make([]type,len)
*/
//1.数组
arr := [3]int{1, 2, 3} //定长
fmt.Println(arr) //[1 2 3]
//2.切片
//定义:
var s1 []int
fmt.Println(s1) //默认是个空切片
//初始化
s2 := []int{1, 2, 3} //和数组的区别是不用指明长度,因为是变长数组
fmt.Println(s2) //[1 2 3]
//从类型看,方括号中带数字长度的是数组,不带数字长度的是切片
fmt.Printf("数组类型:%T, 切片类型%T\n", arr, s2) //数组类型:[3]int, 切片类型[]int
//使用make函数创建切片,make是专门创建引用类型的函数
s3 := make([]int, 6, 8)
fmt.Println(s3) //[0 0 0 0 0 0] 因为长度为6,所以里面默认有六个零值
fmt.Printf("len:%d, cap:%d\n", len(s3), cap(s3)) //len:6, cap:8
//操作切片
//通过下标直接操作
s3[0] = 1
s3[1] = 2
fmt.Println(s3) //[1 2 0 0 0 0]
//fmt.Println(s3[7]) //panic: runtime error: index out of range [7] with length 6
//上面切片虽然容量是8,但是长度只有6,通过下标只能操作切片长度范围内的元素,也就是说只有这些元素有下标
//所以想要继续往切片追加元素,不能通过下标的形式添加,可以使用内置函数append()
//追加一个元素
s3 = append(s3, 7)
//向切片当中添加元素的时候,一旦超过切片的容量阈值,则会自动扩容,会改变底层数组的引用地址,所以append后需要重新引用
fmt.Printf("len:%d, cap:%d, value:%v\n", len(s3), cap(s3), s3) //len:7, cap:8, value:[1 2 0 0 0 0 7]
//添加多个元素
s3 = append(s3, 8, 9, 10, 11, 12, 12) //超过容量会自动扩容
fmt.Printf("len:%d, cap:%d, value:%v\n", len(s3), cap(s3), s3) //len:13, cap:16, value:[1 2 0 0 0 0 7 8 9 10 11 12 12]
//将一个slice的添加进去,
s4 := make([]int, 0, 2)
fmt.Println(s4) //[]
//s4 = append(s4,s3) //cannot use s3 (type []int) as type int in append 直接放切片会报错,因为这是一个切片类型
s4 = append(s4, s3...) //通过骚语法操作
fmt.Printf("len:%d, cap:%d, value:%v\n", len(s4), cap(s4), s4)
//len:13, cap:14, value:[1 2 0 0 0 0 7 8 9 10 11 12 12]
//遍历切片,与数组无异:
for i := 0; i < len(s3); i++ {
println(i)
}
for i, v := range s4 {
fmt.Printf("s4element%d->index:%d, value:%v\n", i, i, v)
}
/*
s4element0->index:0, value:1
s4element1->index:1, value:2
s4element2->index:2, value:0
s4element3->index:3, value:0
s4element4->index:4, value:0
s4element5->index:5, value:0
s4element6->index:6, value:7
s4element7->index:7, value:8
s4element8->index:8, value:9
s4element9->index:9, value:10
s4element10->index:10, value:11
s4element11->index:11, value:12
s4element12->index:12, value:12
*/
}
slice的内存分析及扩容
package main
import "fmt"
func main() {
/*
切片Slice原理:
1.每一个切片在底层都引用了一个数组
2.切片本身不存储任何数据,实际数据存在所引用的底层数组中,所以修改切片实际是对修改底层数组
3.每当向切片中添加数据时,如果没有超过容量则直接添加;如果超出容量,则动态扩容(成倍增长1->2->4->8)
4.扩容本质就新创建一个数组,然后将切片的引用指向新的数组
*/
//代码解释下原理
s1 := []int64{1, 2, 3} //创建一个容量为3的底层数组,内部存储了1,2,3三个元素 ->[3]int{1, 2, 3},然后将数组地址赋值给s1切片
fmt.Printf("len:%d, cap:%d, value:%v\n", len(s1), cap(s1), s1) //len:3, cap:3, value:[1 2 3]
fmt.Printf("address:%p\n",s1) //address:0xc0000be000
fmt.Printf("address:%p\n",&s1) //address:0xc00000c080,&是取值符,此时表示取的是s1本身的地址,并非引用值
s1 = append(s1,6,8)
fmt.Printf("len:%d, cap:%d, value:%v\n", len(s1), cap(s1), s1) //len:5, cap:6, value:[1 2 3 6 8]
fmt.Printf("address:%p\n",s1) //address:0xc000018180 扩容后地址发生了改变
//创建一个容量大于长度的切片
s2 := make([]int,2,3)
fmt.Printf("len:%d, cap:%d\n",len(s2), cap(s2)) //观察其长度和容量 len:2, cap:3
fmt.Printf("address:%p\n",s2) //address:0xc000016480 观察其地址
//添加元素,但不超过容量
s2 = append(s2, 8)
fmt.Printf("len:%d, cap:%d\n",len(s2), cap(s2)) //观察其长度和容量 len:3, cap:3,并未扩容
fmt.Printf("address:%p\n",s2) //address:0xc000016480 观察其地址,发现未改变
//再次追加元素,此时会超过容量
s2 = append(s2,6)
fmt.Printf("len:%d, cap:%d\n",len(s2), cap(s2)) //观察其长度和容量 len:4, cap:6 容量由3->6
fmt.Printf("address:%p\n",s2) //address:0xc000100060 观察其地址,发现扩容后地址发现改变
//再次追加超过容量6
s2 = append(s2, []int{1,2,3}...)
//可以发现超过容量后追加元素,底层数组会成倍的扩容
fmt.Printf("len:%d, cap:%d\n",len(s2), cap(s2)) //观察其长度和容量 len:7, cap:12 容量由6->12
fmt.Printf("address:%p\n",s2) //address:0xc000094060 观察其地址,发现扩容后地址发现改变
}
从原有数组创建切片
package main
import "fmt"
func main() {
/*
可以从已有数组上直接创建切片
slice:= arr[start:end]
将arr从下标start到end范围的元素创建为一个新的切片(前闭后开),长度为end-start
创建为新的切片不意味着底层的数组会被拷贝一份创建一个新的数组,切片引用的底层数组是切分的那个数组
arr[:end],从头到end
arr[start:]从start到末尾
*/
//已有数组直接创建切片
arr := [8]int{1, 2, 3, 4, 5, 6, 7, 8}
fmt.Printf("arr的类型:%T, 长度:%d, 容量:%d\n", arr, len(arr), cap(arr)) //arr的类型:[8]int, 长度:8, 容量:8
s := arr[:] //从头取到末尾
fmt.Printf("s的类型:%T, 长度:%d, 容量:%d\n", s, len(s), cap(s)) //s的类型:[]int, 长度:8, 容量:8
s2 := arr[1:3] //从数组取下标1,2的元素(不包括3),创建为一个新的切片
fmt.Println(s2) //[2 3]
//可以看到切片的容量是从切片的起始位置到数组的末尾
fmt.Printf("s2的类型:%T, 长度:%d, 容量:%d\n", s2, len(s2), cap(s2)) //s2的类型:[]int, 长度:2, 容量:7
s3 := arr[:4] //从头取到下标为3的位置
fmt.Println(s3) //[1 2 3 4]
//结论:切片的容量并非从数组中切出来的范围(那是长度),而是(数组范围)-(起始切的元素下标)
//思考:切片指向的底层数组的地址是否为数组arr地址?
fmt.Printf("arr的地址为(数组是值类型,所以需要取地址):%p\n", &arr) //arr的地址为(数组是值类型,所以需要取地址):0xc00010c000
fmt.Printf("s的地址为:%p\n", s) //s的地址为:0xc00010c000
// 数组的元素之间地址是连续的,64位操作系统int类型占8个字节,所以s2引用的是数组的第二个元素,并非创建了新的数组
fmt.Printf("s2的地址为:%p\n", s2) //s2的地址为:0xc00010c008
fmt.Printf("s3的地址为:%p\n", s3) //s3的地址为:0xc00010c000
//可以看到切片指向的底层数组始终是切分的数组arr,并未生成新的副本
//可以比较下切片的元素对应数组元素之间元素的地址分别是什么
fmt.Printf("arr第一个元素地址:%p\n", &arr[0]) //
//s从数组头部获取到尾部
fmt.Printf("s第一个元素地址:%p,对应arr[0]:%p\n", s, &arr[0]) //s第一个元素地址:0xc000132000,对应arr[0]:0xc000132000
//s2获取数组的第二第三个元素
fmt.Printf("s2第一个元素地址:%p,对应arr[1]:%p\n", s2, &arr[1]) //s2第一个元素地址:0xc00001e108,对应arr[1]:0xc00001e108
//s3从数组的头部获取到数组的第四个元素
fmt.Printf("s3第一个元素地址:%p,对应arr[0]:%p\n", s3, &arr[0]) //s3第一个元素地址:0xc00001e100,对应arr[0]:0xc00001e100
/*
从上面不难发现结论:
从数组生成的切片并未生成一个新的数组副本,而是只是改变切片引用数组的首地址罢了。
例如arr[1:2],那么切片引用的数组对象还是arr,只不过引用的地址并非数组的首元素地址,而是
引用arr[1]这个元素的地址
*/
//还不信?下面分别更改切片共有的元素,如此案例中的第二个元素,观察它们的值是否一起发生改变
s[1] = 100 //数组的第二个元素
fmt.Println("s", s) //s [1 100 100 4 5 6 7 8]
fmt.Println("s2", s2) //s2 [100 3]
fmt.Println("s3", s3) //s3 [1 100 3 4]
s2[1] = 100 //s2因为指向的是arr数组的第二个元素,所以s2[1]改变的是数组的第三个元素
fmt.Println("arr", arr) //arr [1 100 100 4 5 6 7 8] 数组的第三个元素被改变
fmt.Println("s", s) //s [1 100 100 4 5 6 7 8] 切片的第三个元素被改变
fmt.Println("s2", s2) //s2 [1 100 100 4 5 6 7 8] 切片第二个元素被改变
fmt.Println("s3", s3) //s3 [1 100 100 4 5 6 7 8] 切片的第三个元素被改变
//结论:更改切片就更改了共同的底层数组
//改变数组
arr[1] = 101001010
fmt.Println("arr", arr) // [1 101001010 100 4 5 6 7 8]
fmt.Println("s", s) //[1 101001010 100 4 5 6 7 8]
fmt.Println("s2", s2) //s2 [101001010 100]
fmt.Println("s3", s3) //s3 [1 101001010 100 4]
//结论:更改底层数组,就更改了切片对应的值(切片实际不存储数组,指向的是底层数组“某个元素”的地址[划重点:某个元素」)
//如果添加切片元素,达到了扩容,那么就会新数组的副本了,但只是改变该切片的引用,原有切片不会改变引用
fmt.Printf("原s的地址为:%p\n", s)
fmt.Printf("原s2的地址为:%p\n", s2)
fmt.Printf("原s3的地址为:%p\n", s3)
/*
输出:
原s的地址为:0xc0000c0000
原s2的地址为:0xc0000c0008
原s3的地址为:0xc0000c0000
指向的还是同一数组对象
*/
s2 = append(s2, 1, 2, 3, 4, 6, 6, 7, 8, 4, 6) //扩容s2
fmt.Printf("s的地址为:%p\n", s)
fmt.Printf("s2的地址为:%p\n", s2)
fmt.Printf("s3的地址为:%p\n", s3)
/*
输出:
s的地址为:0xc0000c0000
s2的地址为:0xc0000c4000
s3的地址为:0xc0000c0000
s,s3还是指向原来的数组对象,s2因为达到了容量的阈值,所以进行了扩容,创建了一个新的数组副本,所以引用改变
*/
}
切片是引用类型
package main
import "fmt"
func main() {
/*
数据类型:
按类型划分:
基本类型:int, float, string, bool
复合类型:array, slice, map, struct, pointer, function, chan
按特点划分:
值类型:
int, float, string, bool, array, struct
特点:传递的是数据的副本
引用类型:
sclice, map, pointer...(剩下没学到的都是)
特点:传递的是地址,多个变量指向了同一块内存
切片是引用类型的数据,切片本身存储的是底层数组的引用,真实的数据存储在底层数组中
*/
//1.对比先看数组是值类型
a1 := [3]int{1, 2, 3}
a2 := a1
fmt.Println("a1", a1) //a1 [1 2 3]
fmt.Println("a2", a2) //a2 [1 2 3]
a1[0] = 100 //修改a1的值,a2不受影响,因为数组是值类型,传递给a2的是a1的副本
fmt.Println("a1", a1) //a1 [100 2 3]
fmt.Println("a2", a2) //a2 [1 2 3] 看出改变a1对a2毫无作用
//2.切片引用类型
s1 := []int{1, 2, 3}
s2 := s1
fmt.Println("s1", s1) //s1 [1 2 3]
fmt.Println("s2", s2) //s2 [1 2 3]
s1[0] = 100 //修改s1的值,s2的值也跟着改变,因为切片是引用类型,传递给s2的是s1所指向的底层数组的地址
//也就是说s1,s2共同指向了同一个底层数组,所以修改任意一个切片元素的值,都是操纵的同一个底层数组
fmt.Println("s1", s1) //s1 [100 2 3]
fmt.Println("s2", s2) //s2 [100 2 3]
}
拓展:深拷贝和浅拷贝
package main
import "fmt"
func main() {
/*
深拷贝:拷贝的是数值本身
值类型的数据默认都是深拷贝:array,int,float,string,bool,struct
slice实现深拷贝(的方式:
func copy(dst,src[] Type) int
参数一:目标切片 参数二:源切片
result:将源切片的元素拷贝到目标切片当中
浅拷贝:拷贝的是数据的引用(内存地址)
可以使得多个变量指向同一块内存
引用类型的数据默认都是浅拷贝:如slice,map等
*/
//如何实现切片的深拷贝?
//可以通过make方法创建一个新的切片 然后一个值一个值的赋值过去
s1 := []int{1, 2, 3}
s2 := make([]int, 0) // len:0,cap:0
for _, v := range s1 {
s2 = append(s2, v)
}
fmt.Println(s1) //[1 2 3]
fmt.Println(s1) //[1 2 3]
//修改s1的元素,发现s2不会跟着改变,所以实现了深拷贝
s1[0] = 100
fmt.Println(s1) //[100 2 3]
fmt.Println(s2) //[1 2 3]
//上面的方式实现深拷贝太过繁琐,go语言对实现深拷贝提供了内置函数copy()
s3 := []int{6, 7, 8}
fmt.Println(s2) //[1 2 3]
fmt.Println(s3) //[6 7 8]
//将s3的元素拷贝到s2中去
copy(s2, s3) //将s3中的元素,拷贝到s2中
fmt.Println(s2) //[6 7 8]
//改变s3的元素,观察对s2是否有影响
s3[0] = 1000
fmt.Println(s2) //[6 7 8] 毫不影响
fmt.Println(s3) //[1000 7 8]
//也可以只拷贝一部分元素
copy(s2, s3[1:]) //对应将s3的索引为1到末尾的元素拷贝到s2中(注意是从s2的首元素拷贝,而非一一对应)
fmt.Println(s2) //[7 8 8] -》 s3[1:]拷贝到s2的0,1号索引位置
//如果想指定位置拷贝
copy(s2[1:], s3[1:]) //这就索引位置一一对应拷贝了
fmt.Println(s2) //[7 7 8]
}
Map
- Map是go语言内置的数据类型, 它存储的是一个个的键值对。它将一个值与一个键关联起来,使得我们可以使用相应的键来检索值
- Map是无序的键值对集合,其主要作用是通过key来快速检索类型,key类似于索引,指向数据的值
- Map本质上是一组集合,所以我们也可以通过迭代数组和切片那样去循环迭代它,不过Map是无序的,我们无法决定每次迭代返回的元素的顺序
- Map是使用hash表映射实现的,其是引用类型
- 使用Map要注意:
- map是无序的,每次打印出来map都会不一样,其值不能通过索引获取,需要通过key
- map的长度不是固定的,是也会动态扩容,和slice一样,也是以一种引用类型
- 内置的len函数同样适用于map,返回map拥有的key的数量
- map的key可以是所有可比较的类型,如bool、整型、浮点型、复杂类型、字符串...都可以作为键
牛刀小试
package main
import "fmt"
func main() {
/*
map:映射,引用类型,专门存储一组组键值对的集合
存储特点:
A: 存储的是无序的键值对
B: 键不能重复,并且和value值一一对应
map中的key不能重复,如果重复,那么新的value会覆盖原来的,程序不会报错
语法:
1.创建map
声明:
var map_variable map[keyType]valueTYpe
keyType表示key的类型
valueType表示value的类型
通过make函数创建map
map_variable = make(map[keyType]valueTYpe)
注意:如果声明后,未初始化map那么默认会创建一个nil map,nil map不能用来存放键值对
(相当于别的语言的null)
常见数据类型的默认值如下:
int:0
float:0.0-->0
string:""
array:[指定数据类的零值]
[引用类型的默认值是nil]
slice:nil
map:nil
2.初始化:
map := map[string]int {"A":1, "B":2, "C":3}
3.添加/修改
map[key] = value
如果key不存在,则添加数据
如果key存在,则修改数据
4.获取value
map[key]
如果要获取其是否存在 value,ok = map[key]
如果key存在,value就是对应的数据,ok值为true
如果key不存在,value就是值类型的默认值,ok为false
5.删除key
delete(map,key)
key存在则删除,不存在则不影响map
6.获取map长度:key-value的个数
len()
*/
//1.创建map
var map1 map[string]int //nil map,只声明但是未创建map
map2 := make(map[string]int) //已经创建了map
map3 := map[string]int{"A": 1, "B": 2, "C": 3}
fmt.Println(map1) //map[]
fmt.Println(map2) //map[]
fmt.Println(map3) //map[A:1 B:2 C:3]
//2.nil map
//如果声明后,未初始化map那么默认会创建一个nil map,nil map不能用来存放键值对
//map1["age"] = 18 //panic: assignment to entry in nil map
//如何判断一个map是否是空呢?
fmt.Println(map1 == nil) //true
fmt.Println(map2 == nil) //false
fmt.Println(map3 == nil) //false
//所以使用map前 我们要先判断map是否为空
if map1 == nil {
map1 = make(map[string]int)
}
map1["age"] =18 //通过该方式也可以创建或修改键值对(如果key存在则修改,不存在则创建)
fmt.Println(map1) //map[age:18]
//3.存储键值对到map中 map[key] = value
map1["age"] = 18 //添加或修改键值对
map1["height"] = 180
//4.修改数据
map1["age"] = 20 //键值对已存在,此时会覆盖该键值对
fmt.Println(map1) //map[age:20 height:180]
//5.获取数据value
//根据key获取对应的value,如果这个key存在,获取数值,如果key不存在,获取的是value值类型的默认值
fmt.Println(map1["age"]) //20 根据key为“age”,获取相应的value值
//假如获取一个不存在的key对应的value
fmt.Println(map1["瞎xx乱打的key"]) //0 获取的是value对应数据类型的零值
//获取一个不存在的key-value程序不会报错,那如果判断key-value是否存在呢
//我们可以使用ok-idiom获取值,即获取的时候,可以用第二个变量去接收其是否获取成功的布尔值
value, ok := map1["瞎xx乱打的key"]
if ok{
println("key-vlaue存在,value:", value)
}else if !ok{
println("不存在这个玩意儿")
}
//6.删除键值对,使用go提供的内置函数delete
delete(map1,"age") //参数1:操作的map,参数二,要删除的key
fmt.Println(map1) //map[height:180]
//假如删除了一个不存在的key,对map集合没有影响
delete(map1,"不存在的key")
fmt.Println(map1) //map[height:180]
//7.长度,使用len函数获取key-value数量
fmt.Println(len(map1)) //1
}
Map的遍历
package main
import (
"fmt"
"sort"
)
func main() {
/*
map的遍历
map来说,其元素是无序而,所以无法通过下标的方式获取值,只能通过for range
使用 for range
数组和切片,for range获取到的是下标和下标对应的值
map,获取的是key,value
*/
//遍历map
map1 := map[string]string{"name": "Jochen", "age": "22", "height": "181"}
for key, value := range map1 {
fmt.Println(key,value)
}
/*
输出:
name Jochen
age 22
height 181
*/
//如果你硬是想用fori的循环来获取key-value
//1)获取所有的key,存放到切片/数组中
//2)遍历key,获取map[key]的value
keys := make([]string,0,len(map1)) //创建切片
for k, _ := range map1 {
keys = append(keys,k)
}
fmt.Println(keys) //[name age height]
//发现最终骇然得使用forr遍历map...多此一举,有点zz了
//但可以对keys进行一番骚操作,如让其按我们定义的规则有序排列,获取的key-value就变相变得有序了
sort.Strings(keys) //go语言的sort的包提供了排序的功能
//对于字符串来说排序是按字符顺序比较字符编码,如果字符一样,则比较下一个字符知道比较出大小
for i := 0; i < len(keys); i++ {
fmt.Printf("keys:%s,value:%s\n",keys[i],map1[keys[i]])
}
/*
输出:
keys:age,value:22
keys:height,value:181
keys:name,value:Jochen
*/
}
map结合slice使用
package main
import "fmt"
func main() {
/*
map结合slice的使用
1.创建map存储人的信息
name, age, sex, address
2.每个map存储一个人的信息
3.将这些map存入到slice中
4.打印遍历输出
*/
//1.创建三个map存储三个人的信息
people1 := map[string]string{"name":"Jochen","age":"22","sex":"boy","address":"ShenZhen"}
people2 := map[string]string{"name":"Tom","age":"20","sex":"boy","address":"BeiJing"}
people3 := map[string]string{"name":"Timo","age":"18","sex":"girl","address":"SiChuan"}
//将这些人存储在slice中
s1 := make([]map[string]string, 0, 3)
s1 = append(s1,people1)
s1 = append(s1,people2)
s1 = append(s1,people3)
//遍历切片
for i, value := range s1 {
//value: people1,people2...
fmt.Printf("第%d个人的信息:\n", i+1)
fmt.Printf("\t姓名:%s\n", value["name"])
fmt.Printf("\t年龄:%s\n", value["age"])
fmt.Printf("\t性别:%s\n", value["sex"])
fmt.Printf("\t住址:%s\n", value["address"])
}
/*
print:
第1个人的信息:
姓名:Jochen
年龄:22
性别:boy
住址:ShenZhen
第2个人的信息:
姓名:Tom
年龄:20
性别:boy
住址:BeiJing
第3个人的信息:
姓名:Timo
年龄:18
性别:girl
住址:SiChuan
*/
}
map是引用类型
package main
import "fmt"
func main() {
/*
//瞧几个引用类型的模样
array :[size]数据类型
slice: []数据类型
//map也是引用类型
map:map[key数据类型]value数据类型
*/
map1 := make(map[int]float64)
map2 := make(map[string]bool)
fmt.Printf("map1的类型:%T\n", map1) //map1的类型:map[int]float64
fmt.Printf("map2的类型:%T\n", map2) //map2的类型:map[string]bool
fmt.Println("---------------------------")
//1.map是引用类型
p1 := map[string]string{"name": "Jochen", "age": "22", "sex": "boy", "address": "ShenZhen"}
p2 := p1
fmt.Printf("p1的地址%p\n", p1) //p1的地址0xc00010c060
fmt.Printf("p2的地址%p\n", p2) //p2的地址0xc00010c060
fmt.Println(p1)
fmt.Println(p2)
/*
map[address:ShenZhen age:22 name:Jochen sex:boy]
map[address:ShenZhen age:22 name:Jochen sex:boy]
*/
//修改p1的键值对,发现p2的键值对也跟着一起改变了,前面数组和slice奠定那么多,这里应该很敏感了吧
p1["age"] = "18"
//地址未改变
fmt.Printf("p1的地址%p\n", p1) //p1的地址0xc00010c060
fmt.Printf("p2的地址%p\n", p2) //p2的地址0xc00010c060
fmt.Println(p1)
fmt.Println(p2)
/*
map[address:ShenZhen age:18 name:Jochen sex:boy]
map[address:ShenZhen age:18 name:Jochen sex:boy]
*/
}
string字符串
上一篇虽然简单的讲过字符和字符串,但只是初窥门径,string是复杂类型,所以在该小节也理应涵盖,这里主要讲的是string的骚操作
你所不知道的string
package main
import (
"fmt"
"unsafe"
)
func main() {
/*
什么是字符串?
字符串就是一些字节的集合,为什么是字节?因为字符的本质就utf-8的”编码值!“,所以所谓字符没操作的
都是对应的编码值(A-->65,a-->97)
理解为一个字符的序列
每个字符都有固定的位置(索引、下标(从0开始,到长度减1))
go的字符串是一个字节切片(这意味着字符串本质上是切片,其是引用类型)
可以通过将其内容封装在""中来创建字符串,go中的字符串是unicode兼容的,并且是utf-8编码
语法:""或``
``代表原生字符串输出
*/
//1.定义字符串
s1 := "Jochen"
s2 := `Jochen`
fmt.Println(s1) //Jochen
fmt.Println(s2) //Jochen
//2.字符串长度 返回的并非字符的个数,而是字节的个数
fmt.Println(len(s1)) //6 英文字母每个字符占一个字节
s3 := "大黄"
fmt.Println(len(s3)) //6 两个中文字符在utf-8占六个字节
//3.获取一个字节,通过下标
fmt.Println(s3[0]) //"229 " 获取字符串中的第一个字节(字节是编码值,而非字符)
fmt.Printf("%T", s3[0])
//如果要按字符输出,可以使用格式化输出
fmt.Printf("%c\n", s3[1]) //"¤ " 按字符输出字符串中的第一个字符
//别惊讶,s[0]获取的是第一个字节的值(8位二进制的十进制值),%c输出的就是该字节对应的字符
//字符在go中对应的rone类型,其是int32的别称,因为其占用四个字节
c := 'A'
fmt.Println(unsafe.Sizeof(c)) //4
//4.字符串的遍历
for i, v := range s1 {
fmt.Println(i, v)
}
/*
0 74
1 111
2 99
3 104
4 101
5 110
*/
for i := 0; i < len(s1); i++ {
fmt.Printf("%c", s1[i]) //Jochen
}
println()
//5.字符串本质是字节的集合
//汉字每三个字节才能得到一个汉字字符,那么是否可以通过一组字节序列,反向获取字符串?
//当然可以,go提供了内置函数string,将一个字节切片构建字符串
slice1 := []byte{s3[0],s3[1],s3[2]}
s4 := string(slice1) //根据一个字节切片,构建字符串
fmt.Println(slice1,s4) //[229 164 167] 大
//反过来也可以通过字符串转换为字节切片
s5 := "abcdef"
slice2 := []byte(s5) //根据字符串,获取对应的字节切片
fmt.Println(s5,slice2) //abcdef [97 98 99 100 101 102]
//6.字符串不允许修改
fmt.Println(s5) //abcdef
//s5[0] = 'A' // cannot assign to s5[0]
}
strings包的使用
strings包专门为utf-8编码格式的string提供了简单的函数,有很多,记不住就去官方文档看吧
package main
import (
"fmt"
"strings"
)
func main() {
/*
strings包下的字符串函数
*/
s1 := "jochen"
//1.strings.Contains: 判断字符串是否包含指定的内容
fmt.Println(strings.Contains(s1, "chen")) // true
//2.strings.ContainsAny: 是否包含字符序列中任意的一个字符
fmt.Println(strings.ContainsAny(s1, "ozx")) //true
//3.strings.Count:统计substr在sub出现的次数
fmt.Println(strings.Count(s1, "jocx")) //0
//4.strings.HasPrefix:字符串以什么开头
fmt.Println(strings.HasPrefix(s1, "j")) //true
//5.strings.HasSuffix:字符串以什么结尾
fmt.Println(strings.HasSuffix(s1, "n")) //true
//6.strings.Index:查找substr在s中的位置,如果不存在就返回-1
fmt.Println(strings.Index(s1, "j")) //0 从前往后找
fmt.Println(strings.Index(s1, "z")) //-1 未找到返回-1
//7.strings.IndexAny:查找chars中任意一个字符出现在s中的位置,未找到返回-1
//(chars中的字符无论顺序如何,优先返回的是s中出现在前面的索引)
fmt.Println(strings.IndexAny(s1, "sdaojd")) //0
//8.strings.LastIndex查找substr中在s中最后一次出现的位置(多个字符匹配,查找最后一次出现的)
fmt.Println(strings.LastIndex(s1, "o")) //0 也可以认为是从后往前找
//9.strings.Join:字符串拼接
ss := []string{"i", "love", "go"}
var s2 = strings.Join(ss, "-")
fmt.Println(s2) //i-love-go
//10.strings.Split:字符串分割
var ss2 = strings.Split(s2, "-")
fmt.Println(ss2) //[i love go]
//str中不包含指定分隔符,则原样添加到字符串切片中
var ss3 = strings.Split(s2, ",")
fmt.Println(ss3) //[[i-love-go]
//11.strings.Repeat:重复,自己拼接自己
s3 := strings.Repeat("a", 5) //重复拼接5次
fmt.Println(s3) //aaaaa
//12.strings.Replace:替换,最后一个参数为指定替换的次数
s4 := "aaabbbbccc"
s5 := strings.Replace(s4,"a","z",1)
fmt.Println(s5) //zaabbbbccc
s5 = strings.Replace(s4,"a","z",2)
fmt.Println(s5) //zzabbbbccc
//想全部替换则可以使用-1
s5 = strings.Replace(s4,"b","x",-1)
fmt.Println(s5) //aaaxxxxccc
//13.大小写转换(对数字,特殊字符无效)
s6 := strings.ToUpper(s4)
fmt.Println(s6) //AAABBBBCCC
s7 := strings.ToLower(s6)
fmt.Println(s7) //aaabbbbccc
//14.截取字符串,go没有提供像别的语言一样的substring(start,end)方法,因为go有切片
substr := s1[1:]
fmt.Println(substr) //ochen
}
strconv包的使用
在实际开发中,经常需要将字符串与其他基本类型之间进行转换,Go语言通过strconv包下的方法实现这一需求
package main
import (
"fmt"
"strconv"
)
func main() {
/*
strconv包:字符串和基本类型之间的转换
string convert
*/
//go中,+运算符要求操作数数据类型一致(java中可以连接成字符串)
//fmt.Println("1"+1) //invalid operation: "1" + 1 (mismatched types untyped string and untyped int)
//对于整数与字符串相互转换还是常用的下面1,2两个方法
//1.strconv.Atoi:将字符串转换为整型
var a1, e1 = strconv.Atoi("1")
fmt.Println(e1) //<nil> 返回的第二个值为错误值,当没有错误的时候为nil
fmt.Printf("%T,%d\n", a1, a1) //int,1
var a2, e2 = strconv.Atoi("c")
fmt.Println(e2) //strconv.Atoi: parsing "c": invalid syntax 转换失败e返回错误信息
fmt.Printf("%T,%v\n", a2, a2) //int,0 转换失败,将返回的是该类型的零值
//2.strconv.Itoa:将整型转换为字符串
s := strconv.Itoa(-1)
fmt.Printf("%T,%v\n", s, s) //string,-1
//2.字符串转为其他类型
b, err1 := strconv.ParseBool("true")
fmt.Printf("%T,%v,err:%v\n", b, b, err1) //bool,true,err:<nil>
f, err2 := strconv.ParseFloat("3.1415926", 64)//第二个参数表示位数
//第二个参数说明如下:
/*
ParseFloat将字符串s转换为由bitSize指定的精度的浮点数:float32为32或float64为64。
当bitSize = 32时,结果仍为float64类型,但可以将其转换为float32而无需更改其值。
如:下面其他类型转换的bitsize作用也是如此
*/
fmt.Printf("%T,%v,err:%v\n", f, f, err2) //float64,3.141593,err:<nil>
i,err3 := strconv.ParseInt("-1",10,64) //第二个参数表示进制,第三个位数表示位数
fmt.Printf("%T,%v,err:%v\n", i, i, err3) //int64,-1,err:<nil>
u, err4 := strconv.ParseUint("1",10,64)
fmt.Printf("%T,%v,err:%v\n", u, u, err4) //uint64,1,err:<nil>
//4.将其他类型转换为字符串类型 因所有类型都可以转换为字符串显示,所以没有err返回值
s1 := strconv.FormatBool(true)
//第二个参数表示输出格式,E,e为指数格式,f为浮点格式,第三个参数控制精度,一般使用-1使用必要的最小位数,以使ParseFloat准确返回f
s2 := strconv.FormatFloat(3.14,'e',-1,64)
s22 := strconv.FormatFloat(3.14,'f',12,64)
s3 := strconv.FormatInt(1,10) //第二个参数表示进制
s4 := strconv.FormatInt(1,10)
fmt.Printf("%T,%v\n", s1, s1)
fmt.Printf("%T,%v\n", s2, s2)
fmt.Printf("%T,%v\n", s22, s22)
fmt.Printf("%T,%v\n", s3, s3)
fmt.Printf("%T,%v\n", s4, s4)
/*
输出:
string,3.14e+00
string,3.140000000000
string,1
string,1
*/
}
本章主要讲的是复杂类型的认识和使用,在此告一段落,下面开始的章二主要说的是函数与面向对象的内容
学习资料参考:这里