07 数据类型

数据类型

一 数据类型的由来

​ 强调:程序=数据+功能,程序运行就是一系列状态/数据的变化,例如英雄等级由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]) // 李一蛋
上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术