go基础
①、go语言特性?
:::info 1、垃圾回收;
2、goroute天然并发;
3、多个goroute之间通过channel进行通信;
4、多返回值;
:::
②、关键字
:::info
**var**
:变量的声明
语法:var identifier type
:不显示初始化默认初始化为类型零值,也适用于指针,如:<font style="color:#DF2A3F;">var ptr *int</font>
— > <font style="color:#DF2A3F;">fmt.Printf("%p", ptr) -- ></font>
<font style="color:#DF2A3F;">0x0</font>
;
举例:var d int = 8
、var c bool
:::
Var (
a int //默认为 0
b string //默认为 ""
c bool //默认为 false
d = 8
e = "hello world"
)
:::info
**const **
:变量和常量的声明;代表永远是只读的,不能修改;
语法:const identifier [type] = value
,其中type可以省略。
举例:const b string = "hello world"
、const Pi = 3.1414926
类型自动推导;
- 函数外的每个语句都必须以关键字开始(var、const、func等);
- :=不能使用在函数外。
:::
const (
a = 0
b = 1
c = 2
)
:::color5
**iota**
:iota是go语言的常量计数器,只能在常量的表达式中使用。 iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。
:::
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)
const (
n1 = iota //0
n2 //1
_ //使用_跳过某些值
n4 //3
)
const (
n1 = iota //0
n2 = 100 //100 iota声明中间插队
n3 = iota //2
n4 //3
)
const n5 = iota //0
const (
_ = iota
KB = 1 << (10 * iota) // 1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000,也就是十进制的1024
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)
const (
a, b = iota + 1, iota + 2 //1,2
c, d //2,3
e, f //3,4
)
:::info
**fallthrough**
:switch默认的每一个分支后面都带一个隐藏的break;如果不想要break的话就加个 fallthrough,跟c相反;参考:推荐阅读
- 加了fallthrough后,会直接运行【紧跟的后一个】case或default语句,不论条件是否满足都会执行;
- 加了fallthrough语句后,【紧跟的后一个】case条件不能定义常量和变量;
- 执行完fallthrough后直接跳到下一个条件语句,本条件执行语句后面的语句不执行。
:::
:::info
**chan**
用于channel通讯;
:::
:::info
**type**
用于声明自定义类型;包括类似函数指针type add_func func(int, int) int
:::
:::info
**map**
用于声明map类型数据;
:::
:::info
**range**
用于读取slice、map、channel数据;
:::
③、内置函数和接口
内置函数
:::info close:主要用来关闭channel;
:::
:::info delete:从map中删除key对应的value
:::
:::info
len:用来求长度,比如**<font style="color:#DF2A3F;">string</font>****、****<font style="color:#DF2A3F;">array</font>****、slice、map、channel**
;
:::
:::info cap:capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
:::
:::info copy:用于复制和连接slice,返回复制的数目;
:::
:::info
new:用来分配内存,主要用来为值**类型分配内存并初始化指针和内存,比如int、struct
。返回的是**类型指针(一块地址空间的地址,非nil)(指针是引用类型),并且内存对应的值为__类型零值,new出来的也是在堆上的;访问指针的内容用*
操作符,与c一样;
:::
:::info
make:用来分配内存,主要用来分配引用**类型**并初始化,比如chan、map、slice
;返回的是**引用类型本身;因为这三种类型就是引用类型,所以就没有必要返回他们的指针了,同时初始化的值也不是被初始化为0,而是可以被指定初始化**。
:::
:::info append:用来追加元素到数组、slice中,返回修改后的数组、slice;
:::
:::info panic和recover:用来做错误处理;panic停止常规的goroutine;recover允许程序定义goroutine的panic动作;
:::
内置接口
:::color5 error:只要实现了Error()函数,返回值为String的都实现了err接口
:::
type error interface { //只要实现了Error()函数,返回值为String的都实现了err接口
Error() String
}
④、包中函数调用和包访问控制规则
:::warning 包中函数调用:
a. 同一个包中函数,直接调用;
b. 不同包中函数,通过包名+点+函数名进行调用
:::
:::warning 包访问控制规则:
a. 大写意味着这个函数/变量是可导出的
b. 小写意味着这个函数/变量是私有的,包外部不能访问
:::
⑤、函数(+init+main)
函数声明:func 函数名字 (参数列表) (返回值列表){}
func add(a int , b int) int {
}
func add1(arg ...int) int { # 0个或多个参数
}
func add2(a int, arg ...int) int { # 1个或多个参数
}
func add3(a int, b int, arg ...int) int { # 2个或多个参数
}
:::info 可变参数:
其中arg是一个**slice**,我们可以通过 arg[index]
依次访问所有参数;
通过len(arg)
来判断传递参数的个数。
:::
:::info golang函数特点:
a. 不支持重载,一个包不能有两个名字一样的函数
b. 函数是一等公民,函数也是一种类型,一个函数可以赋值给变量
c. 匿名函数
d. 多返回值
:::
:::info defer用途:
当函数返回时,执行defer语句。因此,可以用来做资源清理、关闭文件句柄、锁资源释放、数据库连接释放;
多个defer语句,按先进后出的方式执行;跟栈一样,也好理解,先执行的后关闭后执行的先关闭;
defer语句中的变量,在defer声明时就决定了。
:::
:::warning 闭包:一个函数和与其相关的引用环境组合而成的实体;
:::
:::color5 init函数:go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性。
- init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等;
- 每个包可以拥有多个init函数;
- 包的每个源文件也可以拥有多个init函数;
- 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明);
- 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序;
- init函数不能被其他函数调用,而是在main函数执行之前,自动被调用;
:::
:::color5 init函数和main函数的异同**:**
相同点:
两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
不同点:
init可以应用于任意包中,且可以重复定义多个。
main函数**只能用于main包中,且只能定义一个**。
执行顺序:
- 两个函数的执行顺序:
- 对同一个go文件的init()调用顺序是从上到下的。
- 对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数。
- 对于不同的package,如果不相互依赖的话,按照main包中”先import的后调用”的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。
- 如果init函数中使用了println()或者print()你会发现在执行过程中这两个不会按照你想象中的顺序执行。这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用(使用fmt包的打印函数)。
:::
⑥、下划线
:::color5 下划线在import中:当导入一个包时,该包下的文件里所有init()函数都会被执行,然而,有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。
下划线在代码中:
- 忽略这个变量;
- 用 “_” 占位,而如果用变量的话,不使用,编译器是会报错的。
:::
⑦、值类型和引用类型
值类型和引用类型
:::info
值类型:变量直接存储值,内存通常在栈中分配。基本数据类型int、float、bool、string以及数组和<font style="color:#DF2A3F;">struct</font>
;
引用类型:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配。通过GC回收。基本数据类型<font style="color:#DF2A3F;">指针</font>、slice、map、chan
等都是引用类型。
:::
值传递和引用传递
:::info 无论是值传递,还是引用传递,传递给函数的都是变量的副本;一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
Go默认使用按**值传递来传递参数,也就是传递参数的副本**。在函数中对副本的值进行更改操作时,不会影响到原来的变量。
按引用传递其实也可以称作“按值传递”,只不过该副本是一个地址的拷贝,通过它可以修改这个值所指向的地址上的值。
Go语言中,在函数调用时,引用类型**(slice、map、interface、channel)都默认使用引用传递,另外使用指针也进行引用传递**。
:::
数组的值传递
Golang数组作为参数传入函数时,进行的是值传递,这里与Java数组的引用传递是不同的,示例如下:
package main
import "fmt"
func main() {
arr := [8]int{}
for i := 0; i < 8; i++ {
arr[i] = i
}
fmt.Println(arr)
exchange(arr)
fmt.Println(arr)
}
func exchange(arr [8]int) {
for k, v := range arr {
arr[k] = v * 2
}
}
数组的引用传递
默认情况下Golang的数组传递是值传递,但当我们想要对传入的数组进行修改时,可以使用指针来对数组进行操作,如下:
package main
import "fmt"
func main() {
arr := [8]int{}
for i := 0; i < 8; i++ {
arr[i] = i
}
fmt.Println(arr)
exchangeByAddress(&arr)
fmt.Println(arr)
}
func exchangeByAddress(arr *[8]int) {
for k, v := range *arr {
arr[k] = v * 2
}
}
切片的引用传递
Golang中的切片传递默认是引用传递,进行的是引用传递:
package main
import "fmt"
func main() {
slice := []int{1,2,3,4,5}
fmt.Println(slice)
exchangeSlice(slice)
fmt.Println(slice)
}
func exchangeSlice(slice []int) {
for k, v := range slice {
slice[k] = v * 2
}
}
⑧、变量的作用域
:::info 1、在函数内部声明的变量叫做局部变量,生命周期仅限于函数内部。
2、在函数外部声明的变量叫做全局变量,生命周期作用于整个包,如果是大写的,则作用于整个程序。
:::
⑨、数据类型和操作符
:::info bool类型(1字节,flase),只能存true和false;不允许将整型强制转换为布尔型,布尔型无法参与数值运算,也无法与其他类型进行转换。
:::
:::info
字符类型(1字节,0,uint8的别名):byte
,var a byte = 'c'
:::
:::info rune类型**(4字节,0,int32的别名,UTF-8 字符串):**一个utf-8字符,常处理中文、日文、复合字符;
:::
:::info 数字类型(0),主要有int、int8、int16、int32、int64、uint8、uint16、uint32、uint64、float32、float64
:::
:::info
类型转换,只有强制类型转换,没有隐式类型转换,type(variable)
,比如:var a int=8; var b int32=int32(a)
小转大可以转,正常转
大转小可以转,损失精度
:::
:::info
字符串类型(16字节,””,**UTF-8 字符串**):string
,如var str string
,如var str = "hello world"
字符串表示两种方式: 1)双引号 2)`` (反引号)
双引号是UTF-8编码字符串,转义字符可以生效;
反引号是原生字符串,转义字符失效。
:::
字符串常用操作:
方法 | 介绍 |
---|---|
len(str) | 求长度 |
+或fmt.Sprintf | 拼接字符串 |
strings.Split | 分割 |
strings.Contains | 判断是否包含 |
strings.HasPrefix,strings.HasSuffix | 前缀/后缀判断 |
strings.Index(),strings.LastIndex() | 子串出现的位置 |
strings.Join(a[]string, sep string) | join操作 |
:::info 当一个指针被定义后**没有分配到任何变量时**,它的值为 nil;
slice(nil)、map(nil)、channel(nil):引用类型,空指针值 nil,而非C/C++ NULL
interface(nil):接口类型,接口也是一种类型
function(nil):函数类型
:::
⑩、流程控制
:::info go语言不支持**三元操作符**(三目运算符) “a > b ? a : b”
:::
:::info
for range 语句:用来遍历<font style="color:#DF2A3F;">数组</font>、slice、map、chan、<font style="color:#DF2A3F;">string</font>
;
:::
str := "hello world,中国"
for i, v := range str {
fmt.Printf("index[%d] val[%c]\n", i, v)
}
①、数组与切片
:::warning 数组:是同一种数据类型的固定长度的序列。一旦定义,长度不能变。
语法:var a [len]int
长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型;
访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic;
数组是值类型,因此改变副本的值,不会改变本身的值。
支持指针数组和数组指针:指针数组 [n]T,数组指针 [n]T;
数组初始化:数组初始化 对于数组 []里面肯定要有东西。
:::
// 一维数组
var age0 [5]int = [5]int{1,2,3}
var age1 = [5]int{1,2,3,4,5}
var age2 = [...]int{1,2,3,4,5,6} // 通过后面的值推导···
var str = [5]string{3:"hello world", 4:"tom"} // 下标为3的初始化为hello world
// 多维数组
var f[2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
// 多维数组遍历
var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
for k1, v1 := range f {
for k2, v2 := range v1 {
fmt.Printf("(%d,%d)=%d ", k1, k2, v2)
}
fmt.Println()
}
:::warning 切片:切片是数组的一个引用,因此切片是引用类型;
语法:var 变量名 []类型
,如var str []string
、var arr []int
;
切片的长度可以改变,因此,切片是一个可变的数组;
切片遍历方式和数组一样,可以用len()求长度;
cap可以求出slice最大的容量,0 <= len(slice) <= (array),其中array是slice引用的数组。
切片初始化:var slice []int = arr[start:end]
,左闭右开。
切片**尾删**:slice = slice[:len(slice)-1]
:::
package main
import "fmt"
func main() {
//1.声明切片
var s1 []int
if s1 == nil {
fmt.Println("是空")
} else {
fmt.Println("不是空")
}
// 2.:=
s2 := []int{}
// 3.make()
var s3 []int = make([]int, 0)
fmt.Println(s1, s2, s3)
// 4.初始化赋值
var s4 []int = make([]int, 0, 0)
fmt.Println(s4)
s5 := []int{1, 2, 3}
fmt.Println(s5)
// 5.从数组切片
arr := [5]int{1, 2, 3, 4, 5}
var s6 []int
// 前包后不包
s6 = arr[1:4]
fmt.Println(s6)
}
Var slice []int = arr[0:end] // 可以简写为var slice []int=arr[:end]
Var slice []int = arr[start:len(arr)] // 可以简写为 var slice[]int = arr[start:]
Var slice []int = arr[0, len(arr)] // 可以简写为 var slice[]int = arr[:]
slice = slice[:len(slice)-1] // 如果要切片最后一个元素去掉
s = slice[low:high:max] // 前两个一样,第三个max含义为:cap = max - low
:::warning 切片的内存布局:类似C++ vector
需要注意:底层数组是同一个数组的切片或数组,只要其中更改一个,所有与之相连的数组或切片都会变。
:::
// 切片从数组截取来的情况
data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3]
fmt.Println(s, data) // 没有重新分配底层数组,与原数组相关。// [0 1] [0 1 2 3 4 0 0 0 0 0 0]
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。// 相同 0xc000018240 0xc000018240
s = append(s,100) // 一次 append 一个值,没有超出 s.cap 限制。
fmt.Println(s, data) // 没有重新分配底层数组,与原数组相关。// [0 1 100] [0 1 100 3 4 0 0 0 0 0 0]
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。// 相同 0xc000018240 0xc000018240
s = append(s,100,200) // 一次 append 两个值,超出 s.cap 限制。
fmt.Println(s, data) // 重新分配底层数组,与原数组无关。 //[0 1 100 100 200] [0 1 100 3 4 0 0 0 0 0 0]
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。 // 不相同 0xc000010420 0xc000018240
// 切片从切片截取来的情况,与从数组截取来的相同
data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3]
fmt.Println(s, data) // 没有重新分配底层数组,与原数组相关。// [0 1] [0 1 2 3 4 0 0 0 0 0 0]
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。// 相同 0xc000018240 0xc000018240
s = append(s,100) // 一次 append 一个值,没有超出 s.cap 限制。
fmt.Println(s, data) // 没有重新分配底层数组,与原数组相关。// [0 1 100] [0 1 100 3 4 0 0 0 0 0 0]
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。// 相同 0xc000018240 0xc000018240
s = append(s,100,200) // 一次 append 两个值,超出 s.cap 限制。
fmt.Println(s, data) // 重新分配底层数组,与原数组无关。 //[0 1 100 100 200] [0 1 100 3 4 0 0 0 0 0 0]
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。 // 不相同 0xc000010420 0xc000018240
:::warning 通过make来创建切片:
语法:var slice []type = make([]type, len)
、slice := make([]type, len)
、slice := make([]type, len, cap)
;
:::
// 用append内置函数操作切片
slice = append(slice, 10) // 添加元素
var a = []int{1,2,3}
var b = []int{4,5,6}
a = append(a, b...) // 融合另一个切片
s3 = append(a, 4, 5, 6)
// 切片resize
var a = []int {1,3,4,5}
b := a[1:2]
b = b[0:3] // 切片索引的最大值是cap,而不是len,超过cap索引越界,超过len,但是不超过cap没事
// For range 遍历切片
for index, val := range slice { // index索引,val值
}
// 切片拷贝
s1 := []int{1,2,3,4,5}
s2 := make([]int, 10)
copy(s2, s1) // 从头被覆盖拷贝 s2 [1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
// 要是反过来copy(s1, s2) s1 [0, 0, 0, 0, 0] 多余的被抛弃,截断
:::warning string与slice:string底层就是一个byte的数组,因此,也可以进行切片操作
改变string中的字符值步骤:
1、转成[]byte;
2、[]byte可进行修改;
3、在转回string;
:::
str := "hello world"
s1 := str[0:5]
fmt.Println(s1) // hello
s2 := str[5:]
fmt.Println(s2) // world
// 改变string中的字符值
// string本身是不可变的,因此要改变string中字符,需要如下操作:
str := "hello world"
s := []byte(str)
s[0] = 'o'
str = string(s)
fmt.Println(str) // oello world
:::warning [][]T,是指元素类型为 []T :
:::
package main
import (
"fmt"
)
func main() {
data := [][]int{
[]int{1, 2, 3},
[]int{100, 200},
[]int{11, 22, 33, 44},
}
fmt.Println(data) // [[1 2 3] [100 200] [11 22 33 44]]
}
②、byte、rune类型、字符串string
组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:Go 语言的字符有以下两种:
var a := '中'
var b := 'x'
:::info uint8类型,或者叫 byte 型(1字节),代表了ASCII码的一个字符。
rune类型(4字节),代表一个 UTF-8字符。
:::
因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果。 字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。 要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。
// 遍历字符串
func traversalString() {
s := "pprof.cn博客"
for i := 0; i < len(s); i++ { //byte
fmt.Printf("%v(%c) ", s[i], s[i])
// 112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 229(å) 141() 154() 229(å) 174(®) 162(¢)
}
fmt.Println()
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
// 112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 21338(博) 23458(客)
}
fmt.Println()
}
func changeString() {
s1 := "hello"
// 强制类型转换 , UTF8编码的就强转为[]byte类型 []byte(s1)
byteS1 := []byte(s1)
byteS1[0] = 'H'
fmt.Println(string(byteS1)) // Hello
s2 := "adaf博客"
runeS2 := []rune(s2) //强制类型转换,中文编码的就强转为[]rune []rune(s2)
runeS2[0] = '狗'
runeS2[4] = '你'
runeS2[5] = 'g'
fmt.Println(string(runeS2)) // 狗daf你g
}
③、指针
:::warning
区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。Go语言中的值类型(int、float、bool、string、array、struct)
都有对应的指针类型,如:*int、*int64、*string
等。
Go语言中的指针操作非常简单,只需要记住两个符号:&
(取地址)和*
(根据地址取值)。
要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
指针类型:对变量进行取地址(&)操作,可以获得这个变量的指针变量。
指针地址:指针变量的值是指针地址。
指针取值:对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
:::
:::warning 空指针:当一个指针被定义后没有分配到任何变量时,它的值为 nil
:::
:::color2 看下面这个例子:
执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量**,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储**。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存:::
func main() {
var a *int
*a = 100 // 报错
fmt.Println(*a)
var b map[string]int
b["测试"] = 100 // 报错
fmt.Println(b)
}
:::warning new和make:
new:func new(Type) *Type
:Type
表示类型,new函数只接受一个参数,这个参数是一个类型;*Type
表示类型指针,new函数返回一个指向该类型内存地址的指针。
var a *int
只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值;
:::
go
// 利用new内置函数,new会为该指针初始化
func main() {
var a *int
a = new(int) // 为其初始化
*a = 10
fmt.Println(*a)
}
:::warning
make:func make(t Type, size ...IntegerType) Type
:make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,并为他们初始化,且值也不是被初始化为0,而是可以被指定初始化。
前面例子错误解析:var b map[string]int
只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值;
:::
func main() {
var b map[string]int
b = make(map[string]int, 10) //
b["测试"] = 100
fmt.Println(b)
}
④、map
map是一种无序的(**go的map的底层是hash,所以是无序的**)基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。Go语言中内置的map不是并发安全的。:::warning
make:make(map[KeyType]ValueType, [cap])
;map类型的变量默认初始值为nil,其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
:::
:::warning
判断map中键是否存在的特殊写法:value, ok := map[key]
;
:::
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
}
:::warning map的遍历:for range遍历map。
注意: 遍历map时的元素顺序与添加键值对的顺序无关。
:::
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
//但我们只想遍历key的时候,可以按下面的写法
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
for k := range scoreMap {
fmt.Println(k)
}
}
:::warning
删除键值对:使用delete()内建函数从map中删除一组键值对:delete(map, key)
:::
func main(){
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
delete(scoreMap, "小明")//将小明:100从map中删除
for k,v := range scoreMap{
fmt.Println(k, v)
}
}
:::warning 按照指定顺序遍历map:借助slice将map的key进行排序,按照有序的key进行排序;
:::
package main
import (
"fmt"
"math/rand"
"sort"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano()) //初始化随机数种子
var scoreMap = make(map[string]int, 200)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
value := rand.Intn(100) //生成0~99的随机整数
scoreMap[key] = value
}
// 将会无序遍历
for key := range scoreMap {
fmt.Println(key, scoreMap[key])
}
//取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}
//对切片进行排序
sort.Strings(keys)
//按照排序后的key遍历map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}
:::warning 复合结构:
元素为map类型的切片:
:::
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "王五"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "红旗大街"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
:::warning 值为切片类型的map
:::
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}
⑤、go语言的内存分配有几种?
三种,new,make,以及大括号({});
:::info new:采用New返回的是一个指针,即指向一个被初始化为0值得地址。
make:仅仅用于slice,map,channel的内存分配,与New不同的是,采用make分配内存,并不是返回一个地址,而是直接返回类型值,且值也不是被初始化为0,而是可以被指定初始化。
:::
var v []int = make([]int, 10, 100)
// v是一个slice,根据slice的结构我们可以知道,len被初始化为10, cap被初始化100,而data被指向
// 一个10个元素的数组。假设如果用New会怎么样呢。
var p *[]int = new([]int)
// 这个p一个指针,而里面的值会被初始化为0,那么就相当于*p == nil,一用就会奔溃。
:::info
大括号(Composite literals):与make和new不同,{} 适用性比较广, 并可直接赋值初始化,可以用于<font style="color:#DF2A3F;">slice, map, </font>**<font style="color:#DF2A3F;">struct</font>**
等等;
:::
f := new(File)
v := &File{}
t := &File{fd: fd, name: name} // 返回引用类型
k := File{fd: fd, name: name} // 返回值类型
// 如果不对类型做赋值,那么和new的效果是一样的,f与v是等价的,也是被初始化为0,并返回一个指针。当然如果不想被初始化为0,也可以针对每个值做赋值,如t。
// 不带& , 这样用就行
userInfo := map[string]string{
"username": "pprof.cn",
"password": "123456",
}
fmt.Println(userInfo) //map[password:123456 username:pprof.cn]
fmt.Printf("%T",userInfo) //map[string]string
fmt.Println() //
fmt.Printf("%T",&userInfo) // *map[string]string
fmt.Println() //
fmt.Printf("%p",userInfo) // 0xc0000724b0
fmt.Println() //
fmt.Printf("%p",&userInfo) // 0xc00000a028
// 带& : 自我理解:由于map本身就是引用类型,所以 没必要 用这个&初始化,
// 带&的话相当于返回一个引用的指针了(引用的引用)
userInfo := &map[string]string{
"username": "pprof.cn",
"password": "123456",
}
fmt.Println(userInfo) // &map[password:123456 username:pprof.cn]
fmt.Printf("%T",userInfo) // *map[string]string
fmt.Println() //
fmt.Printf("%T",&userInfo) // **map[string]string
fmt.Println() //
fmt.Printf("%p",userInfo) // 0xc0000ca020
fmt.Println() //
fmt.Printf("%p",&userInfo) // 0xc0000ca018
type uI struct{
username string
password string
}
// 不带&,在栈区创建变量,返回结构体变量
func main() {
userInfo := uI{
username: "pprof.cn",
password: "123456",
}
fmt.Println(userInfo) // {pprof.cn 123456}
fmt.Printf("%T",userInfo) // main.uI
fmt.Println() //
fmt.Printf("%T",&userInfo) // *main.uI
fmt.Println() //
fmt.Printf("%p",userInfo) // %!p(main.uI={pprof.cn 123456})
fmt.Println() //
fmt.Printf("%p",&userInfo) // 0xc0000283e0
}
// 带&,在堆区创建变量,返回结构体指针
func main() {
userInfo := &uI{
username: "pprof.cn",
password: "123456",
}
fmt.Println(userInfo) // &{pprof.cn 123456}
fmt.Printf("%T",userInfo) // *main.uI
fmt.Println() //
fmt.Printf("%T",&userInfo) // **main.uI
fmt.Println() //
fmt.Printf("%p",userInfo) // 0xc0000283e0
fmt.Println() //
fmt.Printf("%p",&userInfo) // 0xc00000a028
}
⑥、类型自定义和类型别名
:::warning
自定义类型:在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type关键字来定义自定义类型。自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:type MyInt int
:将MyInt定义为int类型。
:::
:::warning
类型别名:type TypeAlias = Type
:Go1.9版本添加的新功能,TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。之间见得<font style="color:rgb(51, 51, 51);">rune</font>
和<font style="color:rgb(51, 51, 51);">byte</font>
就是类型别名。
:::
//类型定义
type NewInt int // 可扩展
//类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}
⑦、struct
:::warning
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。Go通过结构体struct和interface实现oop(面向对象编程);
struct的成员(也叫属性或字段)可以是任何类型,如普通类型、复合类型、函数、map、interface、struct等;
:::
:::color1 结构体定义:type定义结构体类型
struct是我们自己定义的类型,不能和其他类型进行强制转换;
结构体字段的可见性:结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
结构体内存对其:和c一样。
:::
type Student struct {
name string
age int // 小写私有成员(对外不可见)
Class string // 首字母大写则该成员为公有成员(对外可见)
}
type person1 struct {
name, city string //同样类型的字段也可以写在一行
age int8
}
// 匿名结构体的定义和实例化,一些临时数据结构等场景下还可以使用匿名结构体
var user struct{Name string; Age int}
:::color1 结构体实例化:只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
&形式也是分配在堆上,**相当于对该结构体类型进行了一次new**实例化操作。 还可以不带&的初始化,不带&初始化是初始化在栈上。:::
// 当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
p7 := &person{
city: "北京",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}
// 初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
p8 := &person{
"pprof.cn",
"北京",
18,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"pprof.cn", city:"北京", age:18}
// 使用这种格式初始化时,需要注意:
/*
1.必须初始化结构体的所有字段。
2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。
3.该方式不能和键值初始化方式混用。*/
:::color1 构造函数:Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
:::
// 函数定义
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}
// 调用
p9 := newPerson("pprof.cn", "测试", 90)
fmt.Printf("%#v\n", p9)
:::color1 方法和接收者:方法(Method)是一种作用于特定类型变量的函数;这种特定类型变量叫做接收者;
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型,同时,方法只能被其接受者调用。
值类型的接受者:调用方法时修改接收者值的任意成员变量,在方法结束后,修改都是无效的,修改的是副本;当没有修改操作时可以使用普通类型的接受者;
指针类型的接受者:调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。
指针方法和值方法都可以在指针或者非指针上被调用。也就是说,方法接收者是指针类型时,指针类型的值也是调用这个方法,反之亦然。
:::
//Person 结构体
type Person struct {
name string
age int8
}
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name) // 当没有修改操作时可以使用普通类型的接受者
}
func main() {
p1 := NewPerson("测试", 25)
p1.Dream()
}
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPerson("测试", 25)
fmt.Println(p1.age) // 25
p1.SetAge(30)
fmt.Println(p1.age) // 30
}
:::color1 任意类型添加方法:在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
注意:非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。所以才需要重新将int类型定义为MyInt类型;:::
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}
:::color1 结构体的匿名字段:结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。:::
//Person 结构体Person类型
type Person struct {
string
int
}
func main() {
p1 := Person{
"pprof.cn",
18,
}
fmt.Printf("%#v\n", p1) //main.Person{string:"pprof.cn", int:18}
fmt.Println(p1.string, p1.int) //pprof.cn 18
}
:::color1 嵌套结构体:一个结构体中可以嵌套包含另一个结构体或结构体指针。
嵌套**匿名**结构体:当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
嵌套结构体的**字段名冲突**:嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
结构体的“**继承**”: 当结构体中的成员也是结构体时,该结构体就继承了这个结构体,继承了其所有的方法与属性,当然有多个结构体成员也就是多继承。
:::
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "pprof",
Gender: "女",
Address: Address{
Province: "黑龙江",
City: "哈尔滨",
},
}
fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名结构体
}
func main() {
var user2 User
user2.Name = "pprof"
user2.Gender = "女"
user2.Address.Province = "黑龙江" //通过匿名结构体.字段名访问
user2.City = "哈尔滨" //直接访问匿名结构体的字段名
fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}
//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address
}
func main() {
var user3 User
user3.Name = "pprof"
user3.Gender = "女"
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
}
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}
:::color1 结构体与JSON序列化:JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号””包裹,使用冒号:分隔,然后**紧接着值(不要有空格);多个键值之间使用英文,分隔**。
:::
//Student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
:::color1
结构体标签(Tag):Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来;Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:key1:"value1" key2:"value2"
tag及json序列化相关:
- 结构体需要序列化的字段要大写,小写不被序列化;
- 没有写tag的结构体成员只要首字母大写也可以序列化,json序列化是默认使用字段名作为key;
- 结构体成员后面通过指定tag实现json序列化该字段时的key为tag里面的key(主要思想就是结构体中的成员首字母小写对外不可见,但是我们把成员定义为首字母大写这样与外界进行数据交互会带来极大的不便,此时tag带来了解决方法)
:::
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
func main() {
s1 := Student{
ID: 1,
Gender: "女",
name: "pprof",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
}
⑧、接口
:::success 接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。并且interface不能包含任何变量。接口是一种抽象的类型。
◼ interface 是方法的集合;
◼ interface是一种类型,并且是指针类型;
◼ interface的 更重要的作用在于多态实现;
◼ interface 不能包含任何变量;
◼ 接口命名习惯以 er 结尾。
:::
:::success interface使用:
◼ 接口的使用不仅仅针对结构体,自定义类型、变量等等都可以实现接口。
◼ 如果一个接口没有任何方法,我们称为空接口,由于空接口没有方法,任意结构体都隐式地实现了空接口。
◼** **要实现一个接口,必须实现该接口里面的所有方法。
对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针(所以说他是指针类型),既无法修改复制品的状态,也无法获取指针。只有当**接口类型变量和指向的对象都为nil时,接口才等于nil**。
空接口**<font style="color:#DF2A3F;">interface{}</font>**
:可以容纳任意对象,它是一个万能容器。比如一个字典的 key 是字符串,但是希望 value 可以容纳任意类型的对象map[string]interface{}
,类似于 Java 语言的 Map 类型,这时候就可以使用空接口类型 interface{}。map的key和value都是空接口这也可以map[interface{}]interface{}
;
:::
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
:::success 接口类型变量:接口类型变量能够存储所有实现了该接口的实例。
声明一个Sayer类型的变量x:var x Sayer
x指向nil
:::
package main
import "fmt"
// Sayer 接口
type Sayer interface {
say()
}
type dog struct {}
type cat struct {}
// dog实现了Sayer接口
func (d dog) say() {
fmt.Println("汪汪汪")
}
// cat实现了Sayer接口
func (c cat) say() {
fmt.Println("喵喵喵")
}
func main() {
var x Sayer // 声明一个Sayer类型的变量x 这时x == nil
a := cat{} // 实例化一个cat
b := dog{} // 实例化一个dog
x = a // 可以把cat实例直接赋值给x 这时x != nil了
x.say() // 喵喵喵
x = b // 可以把dog实例直接赋值给x
x.say() // 汪汪汪
}
:::success 值接收者和指针接收者实现接口:
值接收者:不管是<font style="color:rgb(51, 51, 51);">dog</font>
结构体还是结构体指针*dog
类型的变量都可以赋值给该接口变量;
指针接收者**:*接口变量只能存储`dog<font style="color:rgb(51, 51, 51);">类型的值,</font><font style="color:#DF2A3F;">不能</font><font style="color:rgb(51, 51, 51);">存储</font>
dog`类型(非指针类型)的变量。
:::
func (d dog) move() {
fmt.Println("狗会动")
}
func main() {
var x Mover
var wangcai = dog{} // 旺财是dog类型
x = wangcai // x可以接收dog类型
var fugui = &dog{} // 富贵是*dog类型
x = fugui // x可以接收*dog类型
x.move()
}
func (d *dog) move() {
fmt.Println("狗会动")
}
func main() {
var x Mover
var wangcai = dog{} // 旺财是dog类型
x = wangcai // x不可以接收dog类型,报错
var fugui = &dog{} // 富贵是*dog类型
x = fugui // x可以接收*dog类型
x.move()
// 拓展
var dog1 = x.(*dog) // 接口转变回来,也得转成*dog类型
dog1.move()
}
:::success 类型与接口的关系:
一个类型实现多个接口:一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。
多个类型实现同一接口:不同的类型还可以实现同一接口,同一个interface,不同的类型实现,都可以进行调用,它们都按照统一接口进行操作(多态)。
一个类型嵌入其他类型实现接口:一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
:::
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
type dog struct {
name string
}
// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}
// 实现Mover接口
func (d dog) move() {
fmt.Printf("%s会动\n", d.name)
}
func main() {
var x Sayer
var y Mover
var a = dog{name: "旺财"}
x = a
y = a
x.say()
y.move()
}
// Mover 接口
type Mover interface {
move()
}
type dog struct {
name string
}
type car struct {
brand string
}
// dog类型实现Mover接口
func (d dog) move() {
fmt.Printf("%s会跑\n", d.name)
}
// car类型实现Mover接口
func (c car) move() {
fmt.Printf("%s速度70迈\n", c.brand)
}
func main() {
var x Mover
var a = dog{name: "旺财"}
var b = car{brand: "保时捷"}
x = a
x.move() // 旺财会跑
x = b
x.move() // 保时捷速度70迈
}
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}
// 甩干器
type dryer struct{}
// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}
// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}
// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}
:::success interface接口嵌套和组合: 接口与接口间可以通过嵌套和组合创造出新的接口;
- go语言中的接口可以嵌套,可以理解为继承,子接口拥有父接口的所有方法;
- 如果使用该子接口,必须将父接口和子接口的所有方法都实现。
:::
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
// 接口嵌套(组合)
type animal interface {
Sayer
Mover
}
type cat struct {
name string
}
func (c cat) say() {
fmt.Println("喵喵喵")
}
func (c cat) move() {
fmt.Println("猫会动")
}
func main() {
var x animal
x = cat{name: "花花"}
x.move()
x.say()
}
:::success interface类型转换:由于接口是一般类型,当我们使用接口时候可能不知道它是那个类型实现的,接口类型也有类型转换:
语法:val, err := x.(type)
:接口类型转换为type类型,type可以替换为基本类型或其他可转换类型。将interface{} 转为int,ok可省略 但是省略以后转换失败会报错,true转换成功,false转换失败, 并采用默认值。(这叫类型断言)
:::
:::success 空接口:空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。空接口类型的变量可以存储任意类型的变量。
空接口应用:
- 空接口作为map的值
- 空接口作为函数的参数
:::
func main() {
// 定义一个空接口x
var x interface{}
s := "pprof.cn"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "李白"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
:::success
类型断言:空接口可以存储任意类型的值,获取其存储的具体数据采用接口的类型转换;x.(T)
- x:表示类型为interface{}的变量
- T:表示断言x可能是的类型。
接口值:一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。
:::
func main() {
var x interface{}
x = "pprof.cn"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}
//上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
:::success 注意:关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
:::
⑨、反射
:::success 反射:反射是指在程序运行期(Run time)对程序本身进行访问和修改的能力;
GO 反射的意义:Go 语言的 ORM 库离不开它,Go 语言的 json 序列化库离不开它, fmt包字符串格式化离不开它,Go 语言的运行时更是离不开它。
变量的内在机制:
- 变量包含类型信息和值信息
- 类型信息:是静态的元信息,是预先定义好的
- 值信息:是程序运行过程中动态改变的
反射的目标:
- 获取变量的类型信息,例如这个类型的名称、占用字节数、所有的方法列表、所有的内部字段结构、它的底层存储类型等等。反射可以在运行时动态获取程序的各种详细信息;
- 动态的修改变量内部字段值。比如 json 的反序列化,你有的是对象内部字段的名称和相应的值,你需要把这些字段的值在程序运行时循环填充到对象相应的字段里。(牛)
反射的使用:
<font style="color:rgb(51, 51, 51);">reflect</font>
包封装了反射相关的方法- 获取类型信息:
<font style="color:rgb(51, 51, 51);">reflect.TypeOf</font>
,是静态的; - 获取值信息:
<font style="color:rgb(51, 51, 51);">reflect.ValueOf</font>
,是动态的;获取值信息**,可以不必传入变量的地址;** - 修改值信息:
**SetType**
,**修改值信息**必须要传入变量的地址。
**TypeOf()**
方法返回变量的类型信息得到的是一个类型为 reflect.Type
的变量,
**ValueOf()**
方法返回变量的值信息得到的是一个类型为 reflect.Value
的变量。
:::
package main
import (
"fmt"
"reflect"
)
//反射获取interface类型信息
func reflect_type(a interface{}) {
t := reflect.TypeOf(a)
fmt.Println("类型是:", t) // 类型是: float64
fmt.Printf("%T", t) // *reflect.rtypefloat64
// kind()可以获取具体类型
k := t.Kind()
fmt.Println(k) //float64
switch k {
case reflect.Float64:
fmt.Printf("a is float64\n") //a is float64
case reflect.String:
fmt.Println("string")
}
}
func main() {
var x float64 = 3.4
reflect_type(x)
}
package main
import (
"fmt"
"reflect"
)
//反射获取interface值信息
func reflect_value(a interface{}) {
v := reflect.ValueOf(a)
fmt.Println(v) // 3.4
fmt.Printf("%T",v) // reflect.Value
fmt.Println()
k := v.Kind()
fmt.Println(k) // float64
switch k {
case reflect.Float64:
fmt.Println("a是:", v.Float()) // a是: 3.4
}
}
func main() {
var x float64 = 3.4
reflect_value(x)
}
package main
import (
"fmt"
"reflect"
)
//反射修改值
func reflect_set_value(a interface{}) {
v := reflect.ValueOf(a)
k := v.Kind()
switch k {
case reflect.Float64:
// 反射修改值
v.SetFloat(6.9)
fmt.Println("a is ", v.Float())
case reflect.Ptr:
// Elem()获取地址指向的值
v.Elem().SetFloat(7.9)
fmt.Println("case:", v.Elem().Float()) // case: 7.9
// 地址
fmt.Println(v.Pointer()) // 824633778344
}
}
func main() {
var x float64 = 3.4
// 反射认为下面是指针类型,不是float类型
reflect_set_value(&x) // 这里不能传float类型,那样反射中没有地方可以写
fmt.Println("main:", x) // main: 7.9
}
:::success 反射的好处:
- 为了降低多写代码造成的bug率,做更好的归约和抽象
- 为了灵活、好用、方便,做动态解析、调用和处理
- 为了代码好看、易读、提高开发效率,补足与动态语言之间的一些差别
反射的弊端:
- 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
- Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
- 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。
:::
:::success 结构体与反射的使用:
:::
package main
import (
"fmt"
"reflect"
)
// 定义结构体
type User struct {
Id int
Name string
Age int
}
// 绑方法
func (u User) Hello() {
fmt.Println("Hello")
}
// 传入interface{}
func Poni(o interface{}) {
t := reflect.TypeOf(o)
fmt.Println("类型:", t) // 类型: main.User
fmt.Println("字符串类型:", t.Name()) // 字符串类型: User
// 获取值
v := reflect.ValueOf(o)
fmt.Println(v) // {1 zs 20}
// 可以获取所有属性
// 获取结构体字段个数:t.NumField()
for i := 0; i < t.NumField(); i++ {
// 取每个字段
f := t.Field(i)
fmt.Printf("%s : %v", f.Name, f.Type) // Id : intval : 1 Name : stringval : zs Age : intval : 20
// 获取字段的值信息
// Interface():获取字段对应的值
val := v.Field(i).Interface()
fmt.Println("val :", val)
}
fmt.Println("=================方法====================")
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Println(m.Name) // Hello
fmt.Println(m.Type) // func(main.User)
}
}
func main() {
u := User{1, "zs", 20}
Poni(u)
}
package main
import (
"fmt"
"reflect"
)
// 定义结构体
type User struct {
Id int
Name string
Age int
}
// 匿名字段
type Boy struct {
User
Addr string
}
func main() {
m := Boy{User{1, "zs", 20}, "bj"}
t := reflect.TypeOf(m)
fmt.Println(t) // main.Boy
// Anonymous:匿名
fmt.Printf("%#v\n", t.Field(0)) // reflect.StructField{Name:"User", PkgPath:"", Type:(*reflect.rtype)(0x7c0960), Tag:"", Offset:0x0, Index:[]int{0}, Anonymous:true}
// 值信息
fmt.Printf("%#v\n", reflect.ValueOf(m).Field(0)) // main.User{Id:1, Name:"zs", Age:20}
}
package main
import (
"fmt"
"reflect"
)
// 定义结构体
type User struct {
Id int
Name string
Age int
}
// 修改结构体值
func SetValue(o interface{}) {
v := reflect.ValueOf(o)
// 获取指针指向的元素
v = v.Elem()
// 取字段
f := v.FieldByName("Name")
if f.Kind() == reflect.String {
f.SetString("kuteng")
}
}
func main() {
u := User{1, "5lmh.com", 20}
SetValue(&u)
fmt.Println(u) // {1 kuteng 20}
}
package main
import (
"fmt"
"reflect"
)
// 定义结构体
type User struct {
Id int
Name string
Age int
}
func (u User) Hello(name string) {
fmt.Println("Hello:", name) // Hello: 6666
}
func main() {
u := User{1, "5lmh.com", 20}
v := reflect.ValueOf(u)
// 获取方法
m := v.MethodByName("Hello")
// 构建一些参数
args := []reflect.Value{reflect.ValueOf("6666")}
// 没参数的情况下:var args2 []reflect.Value
// 调用方法,需要传入方法的参数
m.Call(args)
}
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string `json:"name1" db:"name2"`
}
func main() {
var s Student
v := reflect.ValueOf(&s)
// 类型
t := v.Type()
// 获取字段
f := t.Elem().Field(0)
fmt.Println(f.Tag.Get("json")) // name1
fmt.Println(f.Tag.Get("db")) // name2
}
:::success 练习题:解析如下配置文件
:::
⑩、go的异常处理panic & recover
:::success
异常处理介绍:Golang 没有结构化异常,使用 **<font style="color:#DF2A3F;">panic</font>**
抛出错误,**<font style="color:#DF2A3F;">recover</font>**
捕获错误。
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
:::
:::color2
panic:是一个内置函数func panic(v interface{})
,可以抛出任意类型对象。(出现panic**如果不捕获**整个协程就完了)假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行;返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行,直到goroutine整个退出,并报告错误。
:::
:::color2
recover:是一个内置函数,func recover() interface{}
,可以捕获任意类型对象。用来控制一个goroutine的panicking行为,捕获panic的输入值,并且恢复正常的执行,一般的调用建议:
- 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行;
- 可以获取通过panic传递的error;
:::
:::color2 注意:
- 利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 **defer 调用的函数中才有效(只在defer中,不在其调用的函数中都无效,在defer调用的函数中的函数中也无效)。_否则__当panic时,recover无法捕获到panic,__无法防止panic扩散__。**_
- recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数会继续执行 defer 之后的那个点,所以如果需要保护代码段,可将代码块重构成匿名函数,如此可确保后续代码被执 (使用方式)。
- 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
:::
:::color2 案例:
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
捕获函数 recover 只有在延迟调用内**直接调用的函数内**才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
★★★★★如果需要保护代码段,需要将代码块重构成匿名函数,如此可确保后续代码被执; 除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态;Go实现类似 try catch 的异常处理。
如何区别使用 panic 和 error 两种方式?
惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。
:::
package main
import "fmt"
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}
// 输出:defer panic
package main
import "fmt"
func test() {
defer func() {
fmt.Println(recover()) //有效
}()
defer recover() //无效!
defer fmt.Println(recover()) //无效!
defer func() {
func() {
println("defer inner")
recover() //无效!
}()
}()
panic("test panic")
}
func main() {
test()
}
// 输出
/*
defer inner
<nil>
test panic
*/
package main
import "fmt"
func test(x, y int) {
var z int
func() {
defer func() {
if recover() != nil {
z = 0
}
}()
panic("test panic")
z = x / y
return
}()
fmt.Printf("x / y = %d\n", z)
}
func main() {
test(2, 1)
}
package main
import (
"errors"
"fmt"
)
var ErrDivByZero = errors.New("division by zero")
func div(x, y int) (int, error) {
if y == 0 {
return 0, ErrDivByZero
}
return x / y, nil
}
func main() {
defer func() {
fmt.Println(recover()) // 输出division by zero
}()
switch z, err := div(10, 0); err {
case nil:
println(z)
case ErrDivByZero:
panic(err)
}
}
package main
import "fmt"
func Try(fun func(), handler func(interface{})) {
defer func() {
if err := recover(); err != nil {
handler(err)
}
}()
fun()
}
func main() {
Try(func() {
panic("test panic")
}, func(err interface{}) {
fmt.Println(err)
})
}
// 输出:test panic