Golang数据类型
基本数据类型
数据类型
- bit计算机内部数据存储最小单位
- byte 计算机中数据处理的基本单位
- 计算机中以字节位单位存储和解释信息,规定一个字节由八个二进制位构成, 即一个字节等于8个比特(1Byte=8bit)
查看变量的数据类型
var n = 100
fmt.Printf("n 的数据类型是 %T", n)
查看变量的字节大小
var n = 100
fmt.Printf("n 的字节大小是 %d byte", unsafe.Sizeof(n))
整数类型
整型分为以下两个大类:
- 按长度分为:int8、int16、int32、int64
- 还有对应的无符号整型:uint8、uint16、uint32、uint64
类型 | 有无符号 | 占用存储空间 | 表数范围 | 备注 |
---|---|---|---|---|
int | 有 | 32byte/64byte | 根据计算机架构决定 | |
int8 | 有 | 1byte=8bit | -128 ~ 127 | |
int16 | 有 | 2byte=16bit | -32768 ~ 32767 | |
int32 | 有 | 4byte=32bit | -2147483648 ~ 2147483647 | |
int64 | 有 | 8byte=64bit | -9223372036854775808 ~ 9223372036854775807 | |
uint | 无 | 32byte/64byte | 根据计算机架构决定 | |
uint8 | 无 | 1byte=8bit | 0 ~ 255 | |
uint16 | 无 | 1byte=16bit | 0 ~ 65535 | |
int32 | 无 | 4byte=32bit | 0 ~ 4294967295 | |
int64 | 无 | 8byte=64bit | 0 ~ 18446744073709551615 | |
uintptr | 无 | 没有指定具体的bit大小 | 用于存放一个指针 |
-
uintptr类型只有在底层编程是才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。
-
其中,uint8 就是我们熟知的 byte 型,int16 对应C语言中的 short 型,int64 对应C语言中的 long 型。
-
Go 语言也有自动匹配特定平台整型长度的类型—— int 和 uint。
-
逻辑对整型范围没有特殊需求时可以使用 int 和 uint。反之,在二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用 int 和 uint。
-
整型变量在使用时,遵守保小不保大的原则,即:在保证程序正确运行下,尽量使用占用空间小的数据类型。
byte与rune
byte与rune都属于别名类型。byte是uint8的别名类型,而rune是int32的别名类型。
一个rune的类型值即可表示一个Unicode字符。一个Unicode代码点通常由"U+"和一个以十六进制表示法表示的整数表示,例如英文字母'A'的Unicode代码点为"U+0041"。
byte 等同于int8,常用来处理ascii字符
rune 等同于int32,常用来处理unicode或utf-8字符
类型 描述 用途 byte 类似 uint8 常用来处理ascii字符 rune 类似 int32 常用来处理unicode或utf-8字符
浮点类型
Go语言支持两种浮点型数:float32 和 float64。这两种浮点型数据格式遵循 IEEE 754 标准:
- float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32。
- float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64。
- 浮点型的存储分为三部分:符号位+指数位+尾数位,在存储过程中,尾数部分可能丢失,造成精度损失
- golang的浮点型默认为float64类型
- 通常情况下,应该使用float64,因为它比float32更精确
- 0.123可以简写成.123,也支持科学计数法表示:5.1234e2 等价于512.34
类型 | 描述 | 精度 |
---|---|---|
float32 | IEEE-754 32位浮点型数 | 单精度 |
float64 | IEEE-754 64位浮点型数 | 双精度 |
打印浮点数时,可以使用 fmt 包配合动词%f,代码如下:
package main
import (
"fmt"
"math"
)
func main() {
fmt.Printf("%f\n", math.Pi) //按默认宽度和精度输出整型。
fmt.Printf("%.2f\n", math.Pi) //按默认宽度,2 位精度输出(小数点后的位数)。
}
复数类型
复数类型有两个:complex64和complex128。实际上,complex64类型的值会由两个float32类型的值分别表示复数的实数部分和虚数部分。而complex128类型的值会由两个float64类型的值表示复数的实数部分和虚数部分。
复数类型的值一般由浮点数表示的实数部分、加号"+"、浮点数表示的虚数部分以及小写字母"i"组成,比如3.9E+1 + 9.99E-2i。
类型 | 结构 |
---|---|
complex32 | float32实部+虚部 |
complex64 | float64实部+虚部 |
字符类型
Golang中没有专门的字符类型,如果要存储单个字符(字母),一般使用byte来保存。
字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的,也就是说对于传统的字符串是由字符组成的,而Go的字符串不同,它是由字节组成的。
- 字符只能被单引号包裹,不能用双引号,双引号包裹的是字符串
- 当我们直接输出type值时,就是输出了对应字符的ASCII码值
- 当我们希望输出对应字符,需要使用格式化输出
- Go语言的字符使用UTF-8编码,英文字母占一个字符,汉字占三个字符
- 在Go中,字符的本质是一个整数,直接输出时,是该字符对应的UTF-8编码的码值。
- 可以直接给某个变量赋一个数字,然后按格式化输出时%c,会输出该数字对应的unicode字符
- 字符类型是可以运算的,相当于一个整数,因为它们都有对应的unicode码
但是如果我们保存的字符大于255,比如存储汉字,这时byte类型就无法保存,此时可以使用uint或int类型保存
字符类型本质探讨
字符型存储到计算机中,需要将字符对应的码值(整数)找出来
存储:字符 --> 码值 --> 二进制 --> 存储
读取: 二进制 -->码值 --> 字符 --> 读取
字符和码值的对应关系是通过字符编码表决定的(是规定好的)
Go语言的编码都统一成了UTF-8。非常的方便,很统一,再也没有编码乱码的困扰了
布尔类型
布尔型数据在 Go 语言中以 bool 类型进行声明,布尔型数据只有 true(真)和 false(假)两个值。
- bool类型占1个字节
- bool类型适用于逻辑运算,一般用于流程控制
- Go 语言中不允许将整型强制转换为布尔型, cannot convert n (type bool) to type int
- 布尔型无法参与数值运算,也无法与其他类型进行转换。
字符串类型
字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本
- 字符串一旦赋值了,就不能修改了:在Go中字符串是不可变的。
- 字符串的两种标识形式
- 双引号,会识别转义字符
var str = "abc\nabc" //输出时会换行
- 反引号,以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击、输出源代码等效果
var str string = `abc\nabc` //输出时原样输出,不会转义
- 字符串拼接方式"+"
var str string = "hello " + "world"
str += "!"
- 当一行字符串太长时,需要使用到多行字符串,可以使用如下处理
//正确写法
str := "hello" +
" world!"
fmt.Println(str)
//错误写法
str := "hello "
+ "world!"
fmt.Println(str)
基本数据类型默认值与数据类型转换
基本数据类型默认值
在Golang中,数据类型都有一个默认值,当程序员没有赋值时,就会保留默认值,在Golang中,默认值也叫做零值。
基本数据类型默认值如下:
数据类型 | 默认值 |
---|---|
整型 | 0 |
浮点型 | 0 |
字符串 | "" |
布尔类型 | false |
数据类型转换
Golang和Java/C不同,Golang在不同类型的变量之间赋值时需要显式转换。也就是Golang中数据类型不能自动转换。
基本语法:
表达式var_type(var_name) 将值v转换为类型var_type
var_type:就是数据类型,比如int32, int64, float32等等
var_name:就是需要转换的变量
var num int = 42
var float float64 = float64(num)
var ui uint8 = uint8(float)
fmt.Println(num, float, ui)
注意事项
- Go中,数据类型的转换可以是从表示范围小-->表示范围大,也可以 范围大—>范围小
- 被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化!
- 在转换中,比如将int64转成int8,编译时不会报错,只是转换的结果是按溢出处理,和我们希望的结果不一样。
- 数据的转换必须显式转换,不能自动转换
- 定义一个int8类型的整数(var num int8 = 0),如果一直自加1,这个变量的值会是(0...127 -128 -127... 0 ...127)循环往复下去,而不会超过类型最大值的范围
- 其他基本类型转string类型
在程序开发中,我们经常需要将数值型转成string类型,或者将string类型转成数值型。
- 方式1:
func Sprintf(format string, a ...interface{}) string
Sprintf根据format参数生成格式化的字符串并返回该字符串。
示例package main import "fmt" func main() { var num1 int = 99; var num2 float64 = 23.456 var isTrue bool = true var char byte = 'A' var str string str = fmt.Sprintf("%d", num1) fmt.Printf("str类型为 %T str = %q\n",str, str) str = fmt.Sprintf("%f", num2) fmt.Printf("str类型为 %T str = %q\n",str, str) str = fmt.Sprintf("%t", isTrue) fmt.Printf("str类型为 %T str = %q\n",str, str) str = fmt.Sprintf("%d", char) fmt.Printf("str类型为 %T str = %q\n",str, str) }
输出结果为
str类型为 string str = "99" str类型为 string str = "23.456000" str类型为 string str = "true" str类型为 string str = "65"
- 方式2:使用strconv包的函数
package main import ( "fmt" "strconv" ) func main() { var num1 int = 99; var num2 float64 = 23.456 var isTrue bool = true var str string str = strconv.FormatInt(int64(num1), 10) str = strconv.Itoa(num1) fmt.Printf("str类型为 %T str = %q\n",str, str) str = strconv.FormatFloat(num2, 'f', 10, 64) fmt.Printf("str类型为 %T str = %q\n",str, str) str = strconv.FormatBool(isTrue) fmt.Printf("str类型为 %T str = %q\n",str, str) }
输出结果为
str类型为 string str = "99" str类型为 string str = "23.4560000000" str类型为 string str = "23.4560000000" str类型为 string str = "true"
- string类型转其他基本类型
- 方式1:使用strconv包的函数
package main import ( "fmt" "strconv" ) func main() { var str string = "true" var str1 string = "123456" var str2 string = "123.456" var isTrue bool var num int64 var num2 float64 isTrue, _ = strconv.ParseBool(str) fmt.Printf("str类型为 %T str = %v\n",isTrue, isTrue) num, _ = strconv.ParseInt(str1, 10, 64) fmt.Printf("str类型为 %T str = %v\n",num, num) num2, _ = strconv.ParseFloat(str2, 64) fmt.Printf("str类型为 %T str = %v\n",num2, num2) }
数据结果为:
str类型为 bool str = true str类型为 int64 str = 123456 str类型为 float64 str = 123.456
注意:在将string类型转成其它基本数据类型时,要确保string类型能够转成有效的数据。比如,我们可以把”123“转成数字123,但是不能把”hello“转成一个整数,如果这样做,Golang直接将其转成0,其它类型也是一样的道理,float => 0, bool => false
引用数据类型
指针
获取变量的地址,用&,比如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) 则是地址拷贝。