文章目录
1.Golang的数据类型
1.1整型
整型的类型有很多中,包括 int8,int16,int32,int64。我们可以根据具体的情况来进行定义
如果我们直接写 int也是可以的,它在不同的操作系统中,int的大小是不一样的
- 32位操作系统:int -> int32
- 64位操作系统:int -> int64
可以通过unsafe.Sizeof 查看不同长度的整型,在内存里面的存储空间
var num2 = 12
fmt.Println(unsafe.Sizeof(num2))
1.2 浮点型
Go语言支持两种浮点型数:float32和float64。这两种浮点型数据格式遵循IEEE754标准:
float32的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32
。float64的浮点数的最大范围约为1.8e308,可以使用一个常量定义:math.MaxFloat64
打印浮点数时,可以使用fmt包配合动词%f
,代码如下:
var pi = math.Pi
// 打印浮点类型,默认小数点6位
fmt.Printf("%f\n", pi)
// 打印浮点类型,打印小数点后2位
fmt.Printf("%.2f\n", pi)
1.2.1 Golang中精度丢失问题
几乎所有的编程语言都有精度丢失的问题,这是典型的二进制浮点数精度损失问题,在定长条件下,二进制小数和十进制小数互转可能有精度丢失
d := 1129.6
fmt.Println(d*100) //输出112959.99999999
解决方法,使用第三方包来解决精度损失的问题:
package main
import (
"log"
"github.com/shopspring/decimal"
)
func main() {
xdecimal, err := decimal.NewFromString("1129.6")
if err != nil {
log.Println("转化decimal失败", err)
}
ydecimal := decimal.NewFromFloat(100)
resultdecimal := xdecimal.Mul(ydecimal)
log.Println(resultdecimal) //112960
}
更详情的用法参考包:github.com/shopspring/decimal
1.3 布尔类型
定义
var fl = false
if f1 {
fmt.Println("true")
} else {
fmt.Println("false")
}
1.4 字符串类型
Go 语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64等)一样。Go语言里的字符串的内部实现使用UTF-8编码。字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCll码字符,例如:
s1 := "hello"
s1 := "你好"
如果想要定义多行字符串,可以使用反引号
var str = `第一行
第二行`
fmt.Println(str)
字符串常见操作
- len(str):求长度
- +或fmt.Sprintf:拼接字符串
- strings.Split:分割
- strings.contains:判断是否包含
- strings.HasPrefix,strings.HasSuffix:前缀/后缀判断
- strings.Index(),strings.LastIndex():子串出现的位置
- strings.Join():join操作
- strings.Index():判断在字符串中的位置
1.5 byte 和 rune类型
组成每个字符串的元素叫做 “字符”,可以通过遍历字符串元素获得字符。字符用单引号 ‘’ 包裹起来
Go语言中的字符有以下两种类型
- uint8类型:或者叫byte型,代表了ACII码的一个字符
- rune类型:代表一个UTF-8字符
当需要处理中文,日文或者其他复合字符时,则需要用到rune类型,rune类型实际上是一个int32
Go使用了特殊的rune类型来处理Unicode,让基于Unicode的文本处理更为方便,也可以使用byte型进行默认字符串处理,性能和扩展性都有照顾。
需要注意的是,在go语言中,一个汉字占用3个字节(utf-8),一个字母占用1个字节
package main
import "fmt"
func main() {
var a byte = 'a'
// 输出的是ASCII码值,也就是说当我们直接输出byte(字符)的时候,输出的是这个字符对应的码值
fmt.Println(a)
// 输出的是字符
fmt.Printf("%c", a)
// for循环打印字符串里面的字符
// 通过len来循环的,相当于打印的是ASCII码
s := "你好 golang"
for i := 0; i < len(s); i++ {
fmt.Printf("%v(%c)\t", s[i], s[i])
}
// 通过rune打印的是 utf-8字符
for index, v := range s {
fmt.Println(index, v)
}
}
1.5.1 修改字符串
要修改字符串,需要先将其转换成[]rune 或 []byte类型,完成后在转换成string,无论哪种转换都会重新分配内存,并复制字节数组
//转换为 []byte 类型
s1 := "big"
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))
//转换为rune类型
s2 := "你好golang"
byteS2 := []rune(s2)
byteS2[0] = '我'
fmt.Println(string(byteS2))
1.6 基本数据类型转换
数值类型转换
// 整型和浮点型之间转换
var aa int8 = 20
var bb int16 = 40
fmt.Println(int16(aa) + bb)
// 建议整型转换成浮点型
var cc int8 = 20
var dd float32 = 40
fmt.Println(float32(cc) + dd)
建议从低位转换成高位,这样可以避免
转换成字符串类型
第一种方式,就是通过 fmt.Sprintf()
来转换:
// 字符串类型转换
var i int = 20
var f float64 = 12.456
var t bool = true
var b byte = 'a'
str1 := fmt.Sprintf("%d", i)
fmt.Printf("类型:%v-%T \n", str1, str1)
str2 := fmt.Sprintf("%f", f)
fmt.Printf("类型:%v-%T \n", str2, str2)
str3 := fmt.Sprintf("%t", t)
fmt.Printf("类型:%v-%T \n", str3, str3)
str4 := fmt.Sprintf("%c", b)
fmt.Printf("类型:%v-%T \n", str4, str4)
第二种方法就是通过strconv
包里面的集中转换方法进行转换:
// int类型转换str类型
var num1 int64 = 20
s1 := strconv.FormatInt(num1, 10)
fmt.Printf("转换:%v - %T", s1, s1)
// float类型转换成string类型
var num2 float64 = 3.1415926
/*
参数1:要转换的值
参数2:格式化类型 'f'表示float,'b'表示二进制,‘e’表示 十进制
参数3:表示保留的小数点,-1表示不对小数点格式化
参数4:格式化的类型,传入64位 或者 32位
*/
s2 := strconv.FormatFloat(num2, 'f', -1, 64)
fmt.Printf("转换:%v-%T", s2, s2)
字符串转换成int 和 float类型
str := "10"
// 第一个参数:需要转换的数,第二个参数:进制, 参数三:32位或64位
num,_ = strconv.ParseInt(str, 10, 64)
// 转换成float类型
str2 := "3.141592654"
num,_ = strconv.ParseFloat(str2, 10)
2.Golang的流程控制
流程控制是每种编程语言控制逻辑走向和执行次序的重要部分,流程控制可以说是一门语言的“经脉"。
Go 语言中最常用的流程控制有if
和for
,而switch
和goto
主要是为了简化代码、降低重复代码而生的结构,属于扩展类的流程控制。
2.1 for range(键值循环)
Go 语言中可以使用for range遍历数组、切片、字符串、map及通道(channel)。通过for range遍历的返回值有以下规律:
- 数组、切片、字符串返回索引和值。
- map返回键和值。
- 通道(channel)只返回通道内的值。
实例:遍历字符串(默认遍历的是rune类型Unicode编码,for遍历的是byte类型ASCII编码)
var str = "你好golang"
for key, value := range str {
fmt.Printf("%v - %c ", key, value) //一个汉字占3个字节,一个字符1个字节
}
遍历切片(数组)
var array = []string{"php", "java", "node", "golang"}
for index, value := range array {
fmt.Printf("%v %s ", index, value)
}
注意,在Go语言中,没有while语句,我们可以通过for来代替
for {
循环体
}
for循环可以通过break
、goto
、return
、panic
语句退出循环
2.2 switch case
使用switch语句可方便的对大量的值进行条件判断,并且同时一个分支可以有多个值
extname := ".txt"
switch extname {
case ".html": {
fmt.Println(".html")
fallthrought
}
case ".txt",".doc": {
fmt.Println("传递来的是文档")
fallthrought
}
case ".js": {
fmt.Println(".js")
fallthrought
}
default: {
fmt.Println("其它后缀")
}
}
tip:在golang中,break可以不写,也能够跳出case,而不会执行其它的。
如果我们需要使用switch的穿透 fallthrought,fallthrough语法可以执行满足条件的 case 的下一个case,为了兼容c语言中的case设计
extname := ".txt"
switch extname {
case ".html": {
fmt.Println(".html")
fallthrought
}
case ".txt",".doc": {
fmt.Println("传递来的是文档")
fallthrought
}
case ".js": {
fmt.Println(".js")
fallthrought
}
default: {
fmt.Println("其它后缀")
}
}
fallthrought 只能穿透紧挨着的一层,不会一直穿透,但是如果每一层都写的话,就会导致每一层都进行穿透。
2.3 goto:跳转到指定标签
goto 语句通过标签进行代码间的无条件跳转。goto 语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto语句能简化一些代码的实现过程。
var n = 20
if n > 24 {
fmt.Println("成年人")
} else {
goto lable3
}
fmt.Println("aaa")
fmt.Println("bbb")
lable3:
fmt.Println("ccc")
fmt.Println("ddd")
3.Golang的数组
和数组对应的类型是Slice(切片),Slice是可以增长和收缩的动态序列,功能也更灵活,但是想要理解slice工作原理的话需要先理解数组。
3.1 数组定义和初始化
// 数组的长度是类型的一部分
var arr1 [3]int
var arr2 [4]string
fmt.Printf("%T, %T \n", arr1, arr2)
// 数组的初始化 第一种方法
var arr3 [3]int
arr3[0] = 1
arr3[1] = 2
arr3[2] = 3
fmt.Println(arr3)
// 第二种初始化数组的方法
var arr4 = [4]int {10, 20, 30, 40}
fmt.Println(arr4)
// 第三种数组初始化方法,自动推断数组长度
var arr5 = [...]int{1, 2}
fmt.Println(arr5)
// 第四种初始化数组的方法,指定下标
a := [...]int{1:1, 3:5}
fmt.Println(a)
3.2 数组的值类型
数组是值类型,赋值和传参会赋值整个数组,因此改变副本的值,不会改变本身的值
// 数组
var array1 = [...]int {1, 2, 3}
array2 := array1
array2[0] = 3
fmt.Println(array1, array2)
例如上述的代码,我们将数组进行赋值后,该改变数组中的值时,发现结果如下
[1 2 3] [3 2 3]
这就说明了,golang中的数组是值类型,而不是和java一样属于引用数据类型。
3.3 切片定义(引用类型)
在golang中,切片的定义和数组定义是相似的,但是需要注意的是,切片是引用数据类型,如下
// 切片定义
var array3 = []int{1,2,3}
array4 := array3
array4[0] = 3
fmt.Println(array3, array4)
我们通过改变第一个切片元素,然后查看最后的效果
[3 2 3] [3 2 3]
4.Golang的切片
4.1 为什么要使用切片
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。 它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址、长度和容量。
声明切片类型的基本语法如下:
var name [] T
其中:
name:表示变量名
T:表示切片中的元素类型
举例
// 声明切片,把长度去除就是切片
var slice = []int{1,2,3}
fmt.Println(slice)
4.2 基于数组定义切片
由于切片的底层就是一个数组,所以我们可以基于数组来定义切片
// 基于数组定义切片
a := [5]int {55,56,57,58,59}
// 获取数组所有值,返回的是一个切片
b := a[:]
// 从数组获取指定的切片
c := a[1:4]
// 获取 下标3之前的数据(不包括3)
d := a[:3]
// 获取下标3以后的数据(包括3)
e := a[3:]
运行结果
[55 56 57 58 59]
[55 56 57 58 59]
[56 57 58]
[55 56 57]
[58 59]
同理,我们不仅可以对数组进行切片,还可以切片在切片
4.3 切片的长度和容量
切片拥有自己的长度和容量,我们可以通过使用内置的len()
函数求长度,使用内置的cap()
函数求切片的容量。
切片的长度就是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。切片s的长度和容量可通过表达式len(s)
和cap(s)
来获取。
举例
// 长度和容量
s := []int {2,3,5,7,11,13}
fmt.Printf("长度%d 容量%d\n", len(s), cap(s))
ss := s[2:]
fmt.Printf("长度%d 容量%d\n", len(ss), cap(ss))
sss := s[2:4]
fmt.Printf("长度%d 容量%d\n", len(sss), cap(sss))
运行结果
长度6 容量6
长度4 容量4
长度2 容量4
为什么最后一个容量不一样呢,因为我们知道,经过切片后sss = [5, 7] 所以切片的长度为2,但是一因为容量是从2的位置一直到末尾,所以为4
4.4 切片的本质
切片的本质就是对底层数组的封装,它包含了三个信息:
- 底层数组的指针
- 切片的长度(len)
- 切片的容量(cap)
举个例子,现在有一个数组 a := [8]int {0,1,2,3,4,5,6,7},切片 s1 := a[:5],相应示意图如下
切片 s2 := a[3:6],相应示意图如下:
4.5 使用make函数构造切片
我们上面都是基于数组来创建切片的,如果需要动态的创建一个切片,我们就需要使用内置的make函数,格式如下:
make ([]T, size, cap)
其中:
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量
举例:
// make()函数创建切片
fmt.Println()
var slices = make([]int, 4, 8)
//[0 0 0 0]
fmt.Println(slices)
// 长度:4, 容量8
fmt.Printf("长度:%d, 容量%d", len(slices), cap(slices))
需要注意的是,golang中没办法通过下标来给切片扩容,如果需要扩容,需要用到append
slices2 := []int{1,2,3,4}
slices2 = append(slices2, 5)
fmt.Println(slices2)
// 输出结果 [1 2 3 4 5]
同时切片还可以将两个切片进行合并
// 合并切片
slices3 := []int{6,7,8}
slices2 = append(slices2, slices3...)
fmt.Println(slices2)
// 输出结果 [1 2 3 4 5 6 7 8]
需要注意的是,切片会有一个扩容操作,当元素存放不下的时候,会将原来的容量扩大两倍。
4.6 使用copy()函数复制切片
前面我们知道,切片就是引用数据类型
- 值类型:改变变量副本的时候,不会改变变量本身
- 引用类型:改变变量副本值的时候,会改变变量本身的值
如果我们需要改变切片的值,同时又不想影响到原来的切片,那么就需要用到copy函数
// 需要复制的切片
var slices4 = []int{1,2,3,4}
// 使用make函数创建一个切片
var slices5 = make([]int, len(slices4), len(slices4))
// 拷贝切片的值
copy(slices5, slices4)
// 修改切片
slices5[0] = 4
fmt.Println(slices4)
fmt.Println(slices5)
运行结果为
[1 2 3 4]
[4 2 3 4]
4.7 删除切片中的值
Go语言中并没有删除切片元素的专用方法,我们可以利用切片本身的特性来删除元素。代码如下
// 删除切片中的值
var slices6 = []int {0,1,2,3,4,5,6,7,8,9}
// 删除下标为1的值
slices6 = append(slices6[:1], slices6[2:]...)
fmt.Println(slices6)
运行结果
[0 2 3 4 5 6 7 8 9]
4.8 切片的排序算法以及sort包
编写一个简单的冒泡排序算法
func main() {
var numSlice = []int{9,8,7,6,5,4}
for i := 0; i < len(numSlice); i++ {
flag := false
for j := 0; j < len(numSlice) - i - 1; j++ {
if numSlice[j] > numSlice[j+1] {
var temp = numSlice[j+1]
numSlice[j+1] = numSlice[j]
numSlice[j] = temp
flag = true
}
}
if !flag {
break
}
}
fmt.Println(numSlice)
}
在来一个选择排序
// 编写选择排序
var numSlice2 = []int{9,8,7,6,5,4}
for i := 0; i < len(numSlice2); i++ {
for j := i + 1; j < len(numSlice2); j++ {
if numSlice2[i] > numSlice2[j] {
var temp = numSlice2[i]
numSlice2[i] = numSlice2[j]
numSlice2[j] = temp
}
}
}
fmt.Println(numSlice2)
对于int、float64 和 string数组或是切片的排序,go分别提供了sort.Ints()、sort.Float64s() 和 sort.Strings()函数,默认都是从小到大进行排序
var numSlice2 = []int{9,8,7,6,5,4}
sort.Ints(numSlice2)
fmt.Println(numSlice2)
对于降序排列,Golang的sort包可以使用 sort.Reverse(slic e) 来调换slice.Interface.Less,也就是比较函数,所以int、float64 和 string的逆序排序函数可以这样写:
// 逆序排列
var numSlice4 = []int{9,8,4,5,1,7}
sort.Sort(sort.Reverse(sort.IntSlice(numSlice4)))
fmt.Println(numSlice4)
5.关于nil的认识
当你声明了一个变量,但却还并没有赋值时,golang中会自动给你的变量赋值一个默认的零值。这是每种类型对应的零值。
- bool:
false
- numbers:
0
- string:
""
- pointers:
nil
- slices:
nil
- maps:
nil
- channels:
nil
- functions:
nil
nil
表示空,也就是数组初始化的默认值就是nil
var slice2 [] int
fmt.Println(slice2 == nil)
运行结果
true
6.Golang map详解
6.1 map的介绍
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
Go语言中map的定义语法如下:
map[KeyType]ValueType
其中:
- KeyType:表示键的类型
- ValueType:表示键对应的值的类型
map类型的变量默认初始值为nil
,需要使用make()
函数来分配内存。语法为:
make:用于slice、map和channel的初始化
示例如下所示:
// 方式1初始化
var userInfo = make(map[string]string)
userInfo["userName"] = "zhangsan"
userInfo["age"] = "20"
userInfo["sex"] = "男"
fmt.Println(userInfo)
fmt.Println(userInfo["userName"])
// 创建方式2,map也支持声明的时候填充元素
var userInfo2 = map[string]string {
"username":"张三",
"age":"21",
"sex":"女",
}
fmt.Println(userInfo2)
6.2 遍历map
使用for range遍历
// 遍历map
for key, value := range userInfo2 {
fmt.Println("key:", key, " value:", value)
}
6.3 判断map中某个键值是否存在
我们在获取map的时候,会返回两个值,也可以是返回的结果,一个是是否有该元素
// 判断是否存在,如果存在 ok = true,否则 ok = false
value, ok := userInfo2["username2"]
fmt.Println(value, ok)
6.4 使用delete()函数删除键值对
使用delete()内建函数从map中删除一组键值对,delete函数的格式如下所示
delete(map 对象, key)
其中:
- map对象:表示要删除键值对的map对象
- key:表示要删除的键值对的键
示例代码如下
// 删除map数据里面的key,以及对应的值
delete(userInfo2, "sex")
fmt.Println(userInfo2)
6.5 元素为map类型的切片
我们想要在切片里面存放一系列用户的信息,这时候我们就可以定义一个元素为map类型的切片
// 切片在中存放map
var userInfoList = make([]map[string]string, 3, 3)
var user = map[string]string{
"userName": "张安",
"age": "15",
}
var user2 = map[string]string{
"userName": "张2",
"age": "15",
}
var user3 = map[string]string{
"userName": "张3",
"age": "15",
}
userInfoList[0] = user
userInfoList[1] = user2
userInfoList[2] = user3
fmt.Println(userInfoList)
for _, item := range userInfoList {
fmt.Println(item)
}
6.6 值为切片类型的map
我们可以在map中存储切片
// 将map类型的值
var userinfo = make(map[string][]string)
userinfo["hobby"] = []string {"吃饭", "睡觉", "敲代码"}
fmt.Println(userinfo)
示例,统计字符串中单词出现的次数
// 写一个程序,统计一个字符串中每个单词出现的次数。比如 "how do you do"
var str = "how do you do"
array := strings.Split(str, " ")
fmt.Println(array)
countMap := make(map[string]int)
for _, item := range array {
countMap[item]++
}
fmt.Println(countMap)
7.Golang的函数
函数是组织好的、可重复使用的、用于执行指定任务的代码块
Go语言支持:函数、匿名函数和闭包。
7.1 函数定义
Go语言中定义函数使用func关键字,具体格式如下:
func 函数名(参数)(返回值) {
函数体
}
示例
// 求两个数的和
func sumFn(x int, y int) int{
return x + y
}
// 调用方式
sunFn(1, 2)
获取可变的参数,可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后面加...
来标识。
注意:可变参数通常要作为函数的最后一个参数
func sunFn2(x ...int) int {
sum := 0
for _, num := range x {
sum = sum + num
}
return sum
}
// 调用方法
sunFn2(1, 2, 3, 4, 5, 7)
方法多返回值,Go语言中函数支持多返回值,同时还支持返回值命名,函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回
// 方法多返回值
func sunFn4(x int, y int)(sum int, sub int) {
sum = x + y
sub = x -y
return
}
7.2 函数类型和变量
定义函数类型
我们可以使用type关键字来定义一个函数类型,具体格式如下
type calculation func(int, int) int
上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
简单来说,凡是满足这两个条件的函数都是calculation类型的函数,例如下面的add 和 sub 是calculation类型
type calc func(int, int) int
// 求两个数的和
func sumFn(x int, y int) int{
return x + y
}
func main() {
var c calc
c = add
}
方法作为参数
/**
传递两个参数和一个方法
*/
func sunFn (a int, b int, sum func(int, int)int) int {
return sum(a, b)
}
或者使用switch定义方法,这里用到了匿名函数
// 返回一个方法
type calcType func(int, int)int
func do(o string) calcType {
switch o {
case "+":
return func(i int, i2 int) int {
return i + i2
}
case "-":
return func(i int, i2 int) int {
return i - i2
}
case "*":
return func(i int, i2 int) int {
return i * i2
}
case "/":
return func(i int, i2 int) int {
return i / i2
}
default:
return nil
}
}
func main() {
add := do("+")
fmt.Println(add(1,5))
}
7.3 匿名函数
函数当然还可以作为返回值,但是在Go语言中,函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下
func (参数)(返回值) {
函数体
}
匿名函数因为没有函数名,所以没有办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
func main() {
func () {
fmt.Println("匿名自执行函数")
}()
}
Golang中的闭包
7.4 全局变量和局部变量
全局变量的特点:
- 常驻内存
- 污染全局
局部变量的特点
- 不常驻内存
- 不污染全局
闭包
- 可以让一个变量常驻内存
- 可以让一个变量不污染全局
闭包可以理解成 “定义在一个函数内部的函数”。在本质上,闭包就是将函数内部 和 函数外部连接起来的桥梁。或者说是函数和其引用环境的组合体。
- 闭包是指有权访问另一个函数作用域中的变量的函数
- 创建闭包的常见的方式就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量
注意:由于闭包里作用域返回的局部变量资源不会被立刻销毁,所以可能会占用更多的内存,过度使用闭包会导致性能下降,建议在非常有必要的时候才使用闭包。
// 闭包的写法:函数里面嵌套一个函数,最后返回里面的函数就形成了闭包
func adder() func() int {
var i = 10
return func() int {
return i + 1
}
}
func main() {
var fn = adder()
fmt.Println(fn())
fmt.Println(fn())
fmt.Println(fn())
}
最后输出的结果
11
11
11
另一个闭包的写法,让一个变量常驻内存,不污染全局
func adder2() func(y int) int {
var i = 10
return func(y int) int {
i = i + y
return i
}
}
func main() {
var fn2 = adder2()
fmt.Println(fn2(10))
fmt.Println(fn2(10))
fmt.Println(fn2(10))
}
7.5 defer语句
Go 语言中的defer 语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。
// defer函数
fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
fmt.Println("4")
defer将会延迟执行
1
3
4
2
如果有多个defer修饰的语句,将会逆序进行执行
// defer函数
fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("4")
运行结果
1
4
3
2
如果需要用defer运行一系列的语句,那么就可以使用匿名函数
func main() {
fmt.Println("开始")
defer func() {
fmt.Println("1")
fmt.Println("2")
}()
fmt.Println("结束")
}
运行结果
开始
结束
1
2
defer执行时机
在Go语言的函数中return语句在底层并不是原子操作,它分为返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前,具体如下图所示
7.6 panic/revocer处理异常
Go语言中是没有异常机制,但是使用panic / recover模式来处理错误
panic:可以在任何地方引发
recover:只有在defer调用函数内有效
func fn1() {
fmt.Println("fn1")
}
func fn2() {
panic("抛出一个异常")
}
func main() {
fn1()
fn2()
fmt.Println("结束")
}
上述程序会直接抛出异常,无法正常运行
fn1
panic: 抛出一个异常
解决方法就是使用 recover进行异常的监听
func fn1() {
fmt.Println("fn1")
}
func fn2() {
// 使用recover监听异常
defer func() {
err := recover()
if err != nil {
fmt.Println(err)
}
}()
panic("抛出一个异常")
}
func main() {
fn1()
fn2()
fmt.Println("结束")
}
异常运用场景
模拟一个读取文件的方法,这里可以主动发送使用panic 和 recover
func readFile(fileName string) error {
if fileName == "main.go" {
return nil
} else {
return errors.New("读取文件失败")
}
}
func myFn () {
defer func() {
e := recover()
if e != nil {
fmt.Println("给管理员发送邮件")
}
}()
err := readFile("XXX.go")
if err != nil {
panic(err)
}
}
func main() {
myFn()
}
7.7 内置函数
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存、主要用来分配值类型,比如 int、struct ,返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic\recover | 用来处理错误 |
7.8 new和make函数
需要注意的是,指针必须在创建内存后才可以使用,这个和 slice 和 map是一样的
// 引用数据类型map、slice等,必须使用make分配空间,才能够使用
var userInfo = make(map[string]string)
userInfo["userName"] = "zhangsan"
fmt.Println(userInfo)
var array = make([]int, 4, 4)
array[0] = 1
fmt.Println(array)
对于指针变量来说
// 指针变量初始化
var a *int
*a = 100
fmt.Println(a)
执行上面的代码会引发panic,为什么呢?在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。Go 语言中new和make是内建的两个函数,主要用来分配内存。
这个时候,我们就需要使用new关键字来分配内存,new是一个内置的函数,它的函数签名如下:
func new(Type) *Type
其中
- Type表示类型,new函数只接受一个参数,这个参数是一个类型
- *Type表示类型指针,new函数返回一个指向该类型内存地址的指针
实际开发中new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
// 使用new关键字创建指针
aPoint := new(int)
bPoint := new(bool)
fmt.Printf("%T \n", aPoint)
fmt.Printf("%T \n", bPoint)
fmt.Println(*aPoint)
fmt.Println(*bPoint)
本节开始的示例代码中 var a *int 只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的
make和new的区别
- 两者都是用来做内存分配的
- make只能用于slice、map以及channel的初始化,返回的还是这三个引用类型的本身
- 而new用于类型的内存分配,并且内存赌赢的值为类型的零值,返回的是指向类型的指针
8.Golang中的指针
Golang中的指针和C语言差不多,不做赘述。
9.Golang中的日期函数
9.1 time包
时间和日期是我们编程中经常会用到的,在golang中time包提供了时间的显示和测量用的函数。
time.Now获取当前时间
timeObj := time.Now()
year := timeObj.Year()
month := timeObj.Month()
day := timeObj.Day()
fmt.Printf("%d-%02d-%02d \n", year, month, day)
9.2 格式化日期
时间类型有一个自带的方法 Format
进行格式化
需要注意的是Go语言中格式化时间模板不是长久的 Y-m-d H:M:S
而是使用Go的诞生时间 2006年1月2日 15点04分 (记忆口诀:2006 1 2 3 4 5)
/**
时间类型有一个自带的方法 Format进行格式化
需要注意的是Go语言中格式化时间模板不是长久的 Y-m-d H:M:S
而是使用Go的诞生时间 2006年1月2日 15点04分 (记忆口诀:2006 1 2 3 4 5)
*/
timeObj2 := time.Now()
// 24小时值 (15表示二十四小时)
fmt.Println(timeObj2.Format("2006-01-02 15:04:05"))
// 12小时制
fmt.Println(timeObj2.Format("2006-01-02 03:04:05"))
9.3 获取当前时间戳
时间戳是自1070年1月1日(08:00:00GMT)至当前时间的总毫秒数。它也被称为Unix
时间戳
/**
获取当前时间戳
*/
timeObj3 := time.Now()
// 获取毫秒时间戳
unixTime := timeObj3.Unix()
// 获取纳秒时间戳
unixNaTime := timeObj3.UnixNano()
9.4 时间戳转日期字符串
通过将时间戳我们可以转换成日期字符串
// 时间戳转换年月日时分秒(一个参数是秒,另一个参数是毫秒)
var timeObj4 = time.Unix(1595289901, 0)
var timeStr = timeObj4.Format("2006-01-02 15:04:05")
fmt.Println(timeStr)
9.5 日期字符串转换成时间戳
// 日期字符串转换成时间戳
var timeStr2 = "2020-07-21 08:10:05";
var tmp = "2006-01-02 15:04:05"
timeObj5, _ := time.ParseInLocation(tmp, timeStr2, time.Local)
fmt.Println(timeObj5.Unix())
9.6 时间间隔
time.Duration
是time
包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。time.Duration
表示一段时间间隔,可表示的最大长度段大约290年。
time包中定义的时间间隔类型的常量如下:
9.7 时间操作函数
我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求,Go语言的时间对象有提供Add方法如下
func (t Time) Add(d Duration)Time
例如
// 时间相加
now := time.Now()
// 当前时间加1个小时后
later := now.Add(time.Hour)
fmt.Println(later)
同理的方法还有:时间差、判断相等
9.8 定时器
方式1:使用time.NewTicker(时间间隔)来设置定时器
// 定时器, 定义一个1秒间隔的定时器
ticker := time.NewTicker(time.Second)
n := 0
for i := range ticker.C {
fmt.Println(i)
n++
if n>5 {
// 终止定时器
ticker.Stop()
return
}
}
方式2:time.Sleep(time.Second)来实现定时器
for {
time.Sleep(time.Second)
fmt.Println("一秒后")
}
10.Type关键字
在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type
关键字来定义自定义类型。
type myInt int
上面代码表示:将mylnt
定义为int
类型,通过type
关键字的定义,mylnt
就是一种新的类型,它具有int
的特性。
示例:如下所示,我们定义了一个myInt类型
type myInt int
func main() {
var a myInt = 10
fmt.Printf("%v %T", a, a)
}
输出查看它的值以及类型,能够发现该类型就是myInt类型
10 main.myInt
除此之外,我们还可以定义一个方法类型
type myFn func(x,y int) int
func fun(x int, y int)int {
return x + y
}
func main() {
var fn myFn = fun
fmt.Println(fn(1, 2))
}
然后调用并输出
3
类型别名
type
关键字是Golang1.9版本以后添加的新功能,又叫"类型别名"
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有大名、小名、英文名,但这些名字都指的是他本人
type TypeAlias = Type
我们之前见过的rune 和 byte 就是类型别名,他们的底层代码如下
type byte = uint8
type rune = int32
11.Golang中的结构体
Golang中没有“类”的概念,Golang中的结构体和其他语言中的类有点相似。和其他面向对象语言中的类相比,Golang中的结构体具有更高的扩展性和灵活性。(整体也和C语言中的结构体类似,注意下述差别即可)
11.1 结构体的定义
使用type 和 struct关键字来定义结构体,具体代码格式如下所示:
/**
定义一个人结构体
*/
type Person struct {
name string
age int
sex string
}
func main() {
// 实例化结构体
var person Person
person.name = "张三"
person.age = 20
person.sex = "男"
fmt.Printf("%#v", person)
}
注意:结构体首字母可以大写也可以小写,大写表示这个结构体是公有的,在其它的包里面也可以使用,小写表示结构体属于私有的,在其它地方不能使用,例如:
type Person struct {
Name string
Age int
Sex string
}
11.2 实例化结构体
实例化结构体1
刚刚实例化结构体用到了:var person Person
// 实例化结构体
var person Person
person.name = "张三"
person.age = 20
person.sex = "男"
实例化结构体2
我们下面使用另外一个方式来实例化结构体,通过new关键字来实例化结构体,得到的是结构体的地址,格式如下
var person2 = new(Person)
person2.name = "李四"
person2.age = 30
person2.sex = "女"
fmt.Printf("%#v", person2)
输出如下所示,从打印结果可以看出person2是一个结构体指针
&main.Person{name:"李四", age:30, sex:"女"}
需要注意:在Golang中支持对结构体指针直接使用,来访问结构体的成员
person2.name = "李四"
// 等价于
(*person2).name = "李四"
实例化结构体3
使用&
对结构体进行取地址操作,相当于对该结构体类型进行了一次new实例化操作
// 第三种方式实例化
var person3 = &Person{}
person3.name = "赵四"
person3.age = 28
person3.sex = "男"
fmt.Printf("%#v", person3)
实例化结构体4
使用键值对的方式来实例化结构体,实例化的时候,可以直接指定对应的值
// 第四种方式初始化
var person4 = Person{
name: "张三",
age: 10,
sex: "女",
}
fmt.Printf("%#v", person4)
实例化结构体5
第五种和第四种差不多,不过是用了取地址,然后返回的也是一个地址
// 第五种方式初始化
var person5 = &Person{
name: "孙五",
age: 10,
sex: "女",
}
fmt.Printf("%#v", person5)
实例化结构体6
第六种方式是可以简写结构体里面的key
var person6 = Person{
"张三",
5,
"女",
}
fmt.Println(person6)
11.3 结构体方法和接收者
在go语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。所谓方法就是定义了接收者的函数。接收者的概念就类似于其他语言中的this
或者self
。
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表)(返回参数) {
函数体
}
其中
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为p,Connector类型的接收者变量应该命名为c等。、
- 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 非指针类型:表示不修改结构体的内容
- 指针类型:表示修改结构体中的内容
- 方法名、参数列表、返回参数:具体格式与函数定义相同
如果示例所示:
/**
定义一个人结构体
*/
type Person struct {
name string
age int
sex string
}
// 定义一个结构体方法
func (p Person) PrintInfo() {
fmt.Print(" 姓名: ", p.name)
fmt.Print(" 年龄: ", p.age)
fmt.Print(" 性别: ", p.sex)
fmt.Println()
}
func (p *Person) SetInfo(name string, age int, sex string) {
p.name = name
p.age = age
p.sex = sex
}
func main() {
var person = Person{
"张三",
18,
"女",
}
person.PrintInfo()
person.SetInfo("李四", 18, "男")
person.PrintInfo()
}
运行结果为:
姓名: 张三 年龄: 18 性别: 女
姓名: 李四 年龄: 18 性别: 男
注意,因为结构体是值类型,所以我们修改的时候,必须是传入的指针
func (p *Person) SetInfo(name string, age int, sex string) {
p.name = name
p.age = age
p.sex = sex
}
11.4 给任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
type myInt int
func fun(x int, y int)int {
return x + y
}
func (m myInt) PrintInfo() {
fmt.Println("我是自定义类型里面的自定义方法")
}
func main() {
var a myInt = 10
fmt.Printf("%v %T \n", a, a)
a.PrintInfo()
}
11.5 结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就被称为匿名字段
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能一个
/**
定义一个人结构体
*/
type Person struct {
string
int
}
func main() {
// 结构体的匿名字段
var person = Person{
"张三",
18
}
}
结构体的字段类型可以是:基本数据类型,也可以是切片、Map 以及结构体
如果结构体的字段类似是:指针、slice、和 map 的零值都是nil,即还没有分配空间
如果需要使用这样的字段,需要先make,才能使用
/**
定义一个人结构体
*/
type Person struct {
name string
age int
hobby []string
mapValue map[string]string
}
func main() {
// 结构体的匿名字段
var person = Person{}
person.name = "张三"
person.age = 10
// 给切片申请内存空间
person.hobby = make([]string, 4, 4)
person.hobby[0] = "睡觉"
person.hobby[1] = "吃饭"
person.hobby[2] = "打豆豆"
// 给map申请存储空间
person.mapValue = make(map[string]string)
person.mapValue["address"] = "北京"
person.mapValue["phone"] = "123456789"
// 加入#打印完整信息
fmt.Printf("%#v", person)
}
同时还支持结构体的嵌套,如下所示
// 用户结构体
type User struct {
userName string
password string
sex string
age int
address Address // User结构体嵌套Address结构体
}
// 收货地址结构体
type Address struct {
name string
phone string
city string
}
func main() {
var u User
u.userName = "moguBlog"
u.password = "123456"
u.sex = "男"
u.age = 18
var address Address
address.name = "张三"
address.phone = "110"
address.city = "北京"
u.address = address
fmt.Printf("%#v", u)
}
嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名,这个时候为了避免歧义,需要指定具体的内嵌结构体的字段。(例如,父结构体中的字段 和 子结构体中的字段相似)
默认会从父结构体中寻找,如果找不到的话,再去子结构体中在找
如果子类的结构体中,同时存在着两个相同的字段,那么这个时候就会报错了,因为程序不知道修改那个字段的为准。
11.6 结构体的继承
结构体的继承,其实就类似于结构体的嵌套,区别在于嵌套是另一结构体作为该结构体的一属性,需要指明属性名称和类型,而继承没有属性名,只有结构体类型名;继承后子结构体将拥有父结构体的特性。如下所示,我们定义了两个结构体,分别是Animal 和 Dog,其中每个结构体都有各自的方法,然后通过Dog结构体 继承于 Animal结构体
// 用户结构体
type Animal struct {
name string
}
func (a Animal) run() {
fmt.Printf("%v 在运动 \n", a.name)
}
// 子结构体
type Dog struct {
age int
// 通过结构体嵌套,完成继承
Animal
}
func (dog Dog) wang() {
fmt.Printf("%v 在汪汪汪 \n", dog.name)
}
func main() {
var dog = Dog{
age: 10,
Animal: Animal{
name: "阿帕奇",
},
}
dog.run();
dog.wang();
}
运行后,发现Dog拥有了父类的方法
阿帕奇 在运动
阿帕奇 在汪汪汪
11.7 Go中的结构体和Json相互转换
Golang JSON序列化是指把结构体数据转化成JSON格式的字符串,Golang JSON的反序列化是指把JSON数据转化成Golang中的结构体对象
Golang中的序列化和反序列化主要通过“encoding/json”包中的 json.Marshal()
和 son.Unmarshal()
// 定义一个学生结构体,注意结构体的首字母必须大写,代表公有,否则将无法转换
type Student struct {
ID string
Gender string
Name string
Sno string
}
func main() {
var s1 = Student{
ID: "12",
Gender: "男",
Name: "李四",
Sno: "s001",
}
// 结构体转换成Json(返回的是byte类型的切片)
jsonByte, _ := json.Marshal(s1)
jsonStr := string(jsonByte)
fmt.Printf(jsonStr)
}
将字符串转换成结构体类型
// 定义一个学生结构体,注意结构体的首字母必须大写,代表公有,否则将无法转换
type Student struct {
ID string
Gender string
Name string
Sno string
}
func main() {
// Json字符串转换成结构体
var str = `{"ID":"12","Gender":"男","Name":"李四","Sno":"s001"}`
var s2 = Student{}
// 第一个是需要传入byte类型的数据,第二参数需要传入转换的地址
err := json.Unmarshal([]byte(str), &s2)
if err != nil {
fmt.Printf("转换失败 \n")
} else {
fmt.Printf("%#v \n", s2)
}
}
注意
我们想要实现结构体转换成字符串,必须保证结构体中的字段是公有的,也就是首字母必须是大写的,这样才能够实现结构体 到 Json字符串的转换。
11.8 结构体标签Tag
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
key1:"value1" key2:"value2"
结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
注意事项:为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
如下所示,我们通过tag标签,来转换字符串的key
// 定义一个Student体,使用结构体标签
type Student2 struct {
Id string `json:"id"` // 通过指定tag实现json序列化该字段的key
Gender string `json:"gender"`
Name string `json:"name"`
Sno string `json:"sno"`
}
func main() {
var s1 = Student2{
Id: "12",
Gender: "男",
Name: "李四",
Sno: "s001",
}
// 结构体转换成Json
jsonByte, _ := json.Marshal(s1)
jsonStr := string(jsonByte)
fmt.Println(jsonStr)
// Json字符串转换成结构体
var str = `{"Id":"12","Gender":"男","Name":"李四","Sno":"s001"}`
var s2 = Student2{}
// 第一个是需要传入byte类型的数据,第二参数需要传入转换的地址
err := json.Unmarshal([]byte(str), &s2)
if err != nil {
fmt.Printf("转换失败 \n")
} else {
fmt.Printf("%#v \n", s2)
}
}
11.9 嵌套结构体和Json序列化反序列化
和刚刚类似,我们同样也是使用的是json.Marshal()
// 嵌套结构体 到 Json的互相转换
// 定义一个Student结构体
type Student3 struct {
Id int
Gender string
Name string
}
// 定义一个班级结构体
type Class struct {
Title string
Students []Student3
}
func main() {
var class = Class{
Title: "1班",
Students: make([]Student3, 0),
}
for i := 0; i < 10; i++ {
s := Student3{
Id: i + 1,
Gender: "男",
Name: fmt.Sprintf("stu_%v", i + 1),
}
class.Students = append(class.Students, s)
}
fmt.Printf("%#v \n", class)
// 转换成Json字符串
strByte, err := json.Marshal(class)
if err != nil {
fmt.Println("打印失败")
} else {
fmt.Println(string(strByte))
}
}
后续笔记 Golang基础学习笔记(二) ~