数据类型
一 数据类型的由来
强调:程序=数据+功能,程序运行就是一系列状态/数据的变化,例如英雄等级由1升级为10,英雄的存活状态由true变为了false等。
而变量就是控制计算机像人一样记录事物状态的一种机制。
事物的状态各种各样,对应着就应该用不同类型的数据去存,这就是数据类型的由来。
拓展阅读
数据是程序的核心,所有的程序都是围绕数据的操作展开的。
1、站在程序员的角度,程序中的数据是用来控制计算机硬件记录下并且表达出事物状态的,记录越方便、表达越清晰,程序员的开发将会越方便。
2、站在计算机底层硬件的角度去看待数据,数据当然全都是由bit位组成的。为了能够记录下多种状态,数据不是由单个bit位而是由一系列的bit位组成的,并且为了能够准确地读取,组成数据的bit位个数/长度固定,所以计算机一般操作的是固定大小的数,如整数、浮点数、比特数组、内存地址。进一步将这个固定大小的数组织在一起并加以命名,就可以记录下并且很好地表达出更多的状态(比如人的年龄、身高、姓名等),这就是编程语言中数据类型的由来。Go语言提供了丰富的数据类型,这些内置的数据类型,兼顾了硬件的特性和表达复杂数据结构的便捷性。
程序中 计算机内存中
程序员————>某种类型的数据———> 0101010101
// 例如:要求能够在计算机中记录下人的年龄、身高、姓名等状态,并且能够在程序中很好地表达出来这些状态,以方便程序员的开发。
程序员————> 人的年龄int ————> 0101010111...
程序员————> 人的身高float32 ————> 0101011101...
程序员————> 人的姓名string ————> 1111111111...
go属于强类型语言,特定类型的数据会在编译时就确定好内存空间大,这样程序运行时效率就高了,不用费心重新申请内存
二 数据类型总览
基本数据类型:基本数据类型是Go语言世界中的原子
- 1、数字类型
- 整型: int8、 int16、 int、 uint、 uintptr等
- 浮点型: float32、 float64
- 复数: complex64、 complex128
- 2、字符串类型:string,字符类型:byte、rune
- 3、布尔型:bool
复合数据类型:以不同的方式组合基本数据类型得到的就是复合数据类型,又称”派生类型“
- 1、数组
- 2、切片
- 3、map
- 3、结构体
其他类型:
- 1、指针
- 2、接口
- 3、通道
三 快速熟悉数据类型
3.1 基本数据类型
1、整型,用来记录:年龄,等级、号码等整数相关
var age int = 18
var level int = 10
2、浮点型,用来记录:身高、体重、薪资等
var salary float64 = 3.3
var height float32 = 1.78
3、字符串,用来记录:名字、名人名言等描述性质的内容
var name string = "egon"
4、布尔型,用来记录:真与假两种状态,通常都是基于比较运算得到的
var life bool = true
3.2 复合数据类型
1、数组类型,用来存多个相同类型的元素,索引对应元素
比如班级所有同学的名字、一个人的所有爱好等相同属性的多个值
var names [6]string = [6]string{"egon","lili","jack","tom","robin"}
fmt.Println(names)
fmt.Println(names[0])
//fmt.Println(names[-1]) // 不支持负向索引
fmt.Println(names[len(names) - 1]) // 取最后一个元素
// 强调:[3]int 与 [2]int绝对不是一个类型,不能混用,也就是说长度也是数组类型的一部分
var m [2]int = [2]int{11,22}
var n [3]int = [3]int{11,22,33}
m=n // 报错
2、切片类型,用来存多个相同类型的元素,索引对应元素
数组类型是值类型,使用有诸多不便之处,我们通常用切片类型居多
// 1、对一个现有的数据进行切片
var names [6]string = [6]string{"egon","lili","jack","tom","robin"}
var slice []string = names[0:3] // 顾头不顾尾
fmt.Println(slice) // [egon lili jack]
fmt.Println(slice[0])
//fmt.Println(slice[-1]) // 不支持负向索引
fmt.Println(slice[len(slice) - 1])
// 2、直接创建一个切片,隐式指向一个底层数组
var slice1 []string = []string{"egon","lili","jack"}
fmt.Println(slice1[len(slice) - 1])
3、map类型,用来存多个相同通类型的元素,用key对应元素
比如我们想存放一个人的三维、或者一个人多门学科的成绩
// 例1
var sanwei map[string]float64 = map[string]float64{
"xw":300.3,
"yw":66.666,
"tw":800.8, // 不要忘记末尾的逗号
}
fmt.Println(sanwei["xw"])
fmt.Println(sanwei["yw"])
fmt.Println(sanwei["tw"])
// 例2
var scores map[string]int= map[string]int{
"数学":300,
"语文":200,
"英语":100,
}
fmt.Println(scores["数学"])
fmt.Println(scores["语文"])
fmt.Println(scores["英语"])
小结:
(1) 如果我们想存放多个同种属性的值,即多个值类型都一样,那么就需要用到数组、切片、map这三种类型
(2) 至于具体用哪一种呢?
数组、切片都是用索引对应值,索引反映的是位置,如果是想以位置来取值,则使用它们,一般切片更为常用
map是key对应值,key可以对value有描述效果,如果是想以key来取值,则用map类型
那么问题来了,如果我们还是想存放多个值,但是多个值的属性不同,即类型不同呢?这就用到了结构体类型
4、结构体,用来存放多个不同类型的元素,属性对应值
例如存放一个人的名字、年龄、薪资
type Person struct {
name string
age int
salary float64
}
var p Person = Person{
name: "egon",
age: 18,
salary: 3.1, // 不要漏掉末尾的逗号
}
fmt.Println(p.name)
fmt.Println(p.age)
fmt.Println(p.salary)
可以看到结构体其实是一种自定义的类型,你如果接触过python应该知道,在python3中类型就是类,在go中其实也是这样,我们声明的Person结构体其实就当于一个类,事实上go确实也是使用结构来说实现面向对象,这一点我们会在后续章节中详细介绍
3.3 其他类型
接口、通道我们将在后续章节介绍,本小节只介绍指针
(1)关键知识复习:变量名到底是什么?
在《变量声明四大组成部分详解》里我们已经详细介绍过:变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,名字都会被替换成地址。
编译和链接过程的一项重要任务就是找到这些名称所对应的地址,所以名字都是不占用内存空间的,名字只存在与编码阶段,是编程语言为了方便程序开发者而提供的一种编码机制。
复习一下:
对于加法运算c = a + b;将会被转换成类似下面的形式:
0X3000 = (0X1000) + (0X2000);
( )表示取值操作,整个表达式的意思是,取出地址 0X1000 和 0X2000 上的值,将它们相加,把相加的结果赋值给地址为 0X3000 的内存
好了,说到了这里,相信你的脑子里已经把变量名c与内存地址0X3000牢牢划上了等号,这有错吗?没错,但是但是但是,跟着egon来思考一下,针对fmt.Println(c)发生了什么
1、首先明确一点,c只要不在等号左边,那么便是取值操作
go会在c所对应的地址外加一个括号(),代表取值,即c在底层是这个样子(0X3000)
2、然后go会将(0X3000)取到的内容拷贝一个副本,再将副本传给Println函数,一定注意是副本,这句话使用所有传参操作。
所以,听明白没有,虽然变量名代表的就是内存地址,但是当我们的变量名不是出现在等号左边时,那都是取值操作,会在其对应地址的基础上加一个括号,变成这种形式:(0X3000),然后还没有完,在基于(0X3000)取到内容后,会将取出的内容拷贝一个副本,然后基于副本进行后续操作,这一点真的非常重要,关系到对后续很多知识的理解,请反复阅读。
(2)什么是指针
上一小节我们再次重申了:变量名只是一种助记符,在编译完毕后根本没有变量名这种东西,只有内存地址,但问题的关键是,我们使用变量名时,都是取到它对应的值,那如果我们就想取到变量的地址,也就是说我们想明确地告诉go:你不要自作多情帮我在变量名对应地址的外层加个括号来取内容了,我就是想要变量的地址。
怎么办?能不能做到?能,使用&符号就可以,如下&name取到的地址就称之为指针
var name string = "egon" // 底层 var 0xc000010200 string = "egon"
fmt.Println(name) // (0xc000010200)取到内容"egon",然后拷贝一个副本传给函数
fmt.Println(&name) // 0xc000010200
使用*
就可以取到地址对应的值,*(&name)的作用是取值,这种写法才等同于引用变量名name
fmt.Println(*(&name)) // egon
*(&name) = "EGON"
fmt.Println(name) // EGON
我们也可以把指针存给另外一个变量,即指针变量
var x int = 100 // 变量内存空间中存发的是一个值100
var p *int = &x // 变量p内存空间中存储的其实也是一个值,只不过这个值是个内存地址罢了。
那么指针有啥用?用处就是传地的开销小、而且可以用来改原值,我们来看两个例子
例1
package main
import "fmt"
func f1(m int) {
m = 666
}
func main() {
var x int = 100
f1(x) // 先加括号(x的内存地址)取到内存100,然后复制一个副本传给函数f1,在函数内修改的是副本,肯定不影响原值
fmt.Println(x)
}
例2
package main
import "fmt"
func f1(m *int) {
*m = 666
}
func main() {
var x int = 100
f1(&x) // 取到变量的地址,然后复制一个副本传给函数f1,副本也还是地址,也还是指向原来变量,在函数内会修改原值
fmt.Println(x) // 666
}
综上,我们得知,函数传递的本质其实都是把“值”拷贝一个副本传入,本质其实都是“值”传递只不过这个“值”有可能是一个值,有可能是一个地址,当传递的“值”是一个地址时,有的文献里也称之为引用传递,这一点知晓即可。
面试题:问变量名是地址的助记符,变量名代表的就是地址,而指针是取出也是变量的地址,他俩有啥区别,岂不是一样了?
答案:有区别
1、
指针与变量名都是地址,不过引用变量名x时,编译器会先取到x的地址,然后再为其加一个取值得符号,也就是(),变成这种形式(地址)
而取&x得到的就是一个地址,不会加括号
也就是说变量名x确实对应的是地址,这个地址与&x取出的确实也是同一个,但是针对变量名x,编译器就是会把变量的地址放入一个括号,以后程序执行时,碰到地址加括号就知道应该去取出值
2、再看下面的例子:
假设变量 a、b、c 在内存中的地址分别是 0X1000、0X2000、0X3000,那么加法运算c = a + b;将会被转换成类似下面的形式: 0X3000 = (0X1000) + (0X2000); ( )表示取值操作,整个表达式的意思是,取出地址 0X1000 和 0X2000 上的值,将它们相加,把相加的结果赋值给地址为 0X3000 的内存
3、
*(&x)的作用是取值,这种写法才等同于引用变量名
4、
变量名只是地址助记符,不存任何东西,编译后就没了
指针变量才是将变量地址存下来的
5、
指针的作用在于引用传递
记住一点,只要亮出名字,他的底层都是括号加地址,代表取值,如(地址)
引用普通变量名,底层会加上括号:(名字所对应的地址)
引用指针变量名,底层也一样,都是:(名字对应的地址)
两种取出来的都是值,只不过这个值,有可能是值,有可能是一个地址
四 数据类型嵌套
例1:存放班级所有同学的成绩,要求以后按位置取第n个学生的成绩,具体成绩则按照名字取
// 切片套map
var scores []map[string]float64 = []map[string]float64{
{"数学": 300, "语文": 200, "英语": 100},
{"数学": 100, "语文": 666, "英语": 300},
{"数学": 200, "语文": 300, "英语": 100},
}
fmt.Println(scores[1]["语文"]) // 666,取出第二名学生的语文成绩
例2:存放班级所有同学的成绩,要求以后按位置取第n个学生的成绩,具体成绩则按照位置取
// 方案1:数组套数组,又称之为多维数组
var scores [3][3]float64 = [3][3]float64{
{300, 200, 100},
{100, 666, 300},
{200, 300, 100},
}
fmt.Println(scores[1][1]) // 666,取出二名学生的第二门成绩
// 方案2:切片套切片
var scores [][]float64 = [][]float64{
{300, 200, 100},
{100, 666, 300},
{200, 300, 100},
}
fmt.Println(scores[1][1]) // 666,取出二名学生的第二门成绩
例3:存放班级所有同学的成绩,要求以后按名字取对应学生的成绩,具体成绩按照名字取
// 方案一:map内嵌套map
var scores map[string]map[string]float64 = map[string]map[string]float64{
"张三": {"数学": 300, "语文": 200, "英语": 100},
"egon": {"数学": 100, "语文": 666, "英语": 300},
"王五": {"数学": 200, "语文": 300, "英语": 100},
}
fmt.Println(scores["egon"]["语文"]) // 666,取出egon的语文成绩
// 方案二:map内嵌套结构体
type score struct {
mathematics float64 // 数学
chinese float64 // 语文
english float64 // 英语
}
var scores map[string]score = map[string]score{
"张三": {mathematics: 300, chinese: 200, english: 100},
"egon": {300, 666, 100},
"王五": {300, 200, 100},
}
fmt.Println(scores["egon"].chinese) // 666,取出egon的语文成绩
例4:存放班级所有同学的成绩,要求以后按名字取对应学生的成绩,具体成绩按照位置取
// 方案二:map内嵌套切片
var scores map[string][]float64 = map[string][]float64{
"张三":[]float64{300,200,100},
"egon":{300,666,100},
"王五":{300,200,100},
}
fmt.Println(scores["egon"][1]) // 200
// 其他方案自行脑补吧,翻来覆去就那些东西
例5:定义结构体,存放一个人的名字、年龄、薪资、爱好、住址
type Person struct {
name string
age int
salary float64
hobbies []string
addr map[string]string
}
var p Person = Person{
name: "egon",
age: 18,
salary: 3.3,
hobbies: []string{"play", "music", "read"},
addr: map[string]string{"province": "山东", "city": "烟台"},
}
fmt.Println(p.hobbies[1]) // music
fmt.Println(p.addr["city"]) // 烟台
例6:指针相关嵌套的例子
a := 10
b := 20
c := 30
// 定义一个数组,数组的元素为指针
var x [3]*int = [3]*int{&a, &b, &c}
fmt.Println(x) // [0xc00001c060 0xc00001c068 0xc00001c070]
// 定义一个类型为数组的指针,简称指针数组
var arr = [3]int{111,222,333} // 先造出一个数组,
var y *[3]int = &arr // 然后取数组的地址赋值给指针数组
fmt.Printf("%p\n",&arr) // 0xc0000181a0
fmt.Printf("%p\n",y) // 0xc0000181a0
fmt.Printf("%p\n",&y) // 问,打印出的这个地址是个啥,与上面的地址一样吗???
注意:slice与map根本没必要取地址,它俩就是为了引用传递而生的,指针切片与指针map都是没啥意义的。
练习:
1、存放女朋友们的三维
2、解释下面两个东西
var x [3]*int
var y *[3]int
3、存放一个人的信息,名字为“egon”,年龄为18,身高为1.83,爱好为指针数组
4、针对下述二维数组/三行两列,取出”李一蛋“
a := [3][2]string{
{"林大牛", "林二牛"},
{"李一蛋", "李二蛋"},
{"王一炮", "王三炮"},
}
fmt.Println(a) // [[林大牛 林二牛] [李一蛋 李二蛋] [王一炮 王三炮]]
fmt.Println(a[1][0]) // 李一蛋