一、Go基础
注释:
// 单行注释
/**/ 多行注释
标识符:
go语言中的标识符由字母数字和下划线(_)组成,并且只能以字母和下划线(_)开头
关键字:
关键字是指编程语言中预先定义好的具有特殊含义的标识符。关键字和保留字都不建议用作变量名。
Go语言有25个关键字:
关键字 | ||||
---|---|---|---|---|
break | default | func | interface | select |
case | defer | go | map | struct |
chan | else | goto | package | switch |
const | fallthrough | if | range | type |
continue | for | import | return | var |
Go语言有37个保留字:
类型 | 包含保留字 |
---|---|
Constants | true false iota nil |
Types | int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex128 complex64 bool byte rune string error |
Functions | make len cap new append copy close delete complex real imag panic recover |
1、变量声明
变量的来历
程序运行过程中的数据都是保存在内存中的,我们想要在代码中操作某个数据时就要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能在找到内存上对应的数据了。
变量类型
变量(Variable)的功能是存储数据。不同的变量保存数据类型可能会不一样。经过半个多世纪的发展,编程语言已经基本形成了一套固定的类型,常见的变量的数据类型有:整形、浮点型、布尔型等。
Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。
变量的声明
Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。并且Go语言的变量声明后必须使用。
标准声明
Go语言的变量声明格式为:
var 变量名 变量类型
变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。
var name string var age int var isOK bool
批量声明
每声明一个变量就需要写var关键字会比较繁琐,go语言中还支持批量变量的声明
var( a string b int c bool d float32 )
变量的初始化
Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始划操作。每个变量会被初始化成其类型的默认值,例如:整形和浮点类型变量的默认值为0。字符串变量的默认值为空字符串。布尔类型莫认为false。切片、函数、指针变量默认为nil。
也可以在声明变量的时候为其指定初始值。变量初始化的标准格式如下:
var 变量名 类型 = 表达式 // 例 var username string = "eric" var age int = 18
也能一次初始化多个变量
var name, age = "eric", 18
类型推导
有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。
var name = "eric" var age = 18
短变量声明
在函数内部,可以使用跟简略的:=方式声明并初始化变量。
package main import ("fmt") // Go语言函数外部的语句必须以关键字开头 // 全局变量 var m = 100 const( num = 18 ) // main函数是入口函数,他没有参数也没有返回值 func main(){ // 函数内部定义的变量必须使用 n := 10 m := 200 //此处声明局部变量m fmt.PrintLn(m,n) }
匿名变量
在使用多重赋值的时候,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。匿名变量用一个下划线_表示,例如:
func foo() (int, string){ return 18, "eric" } func main() { x,_ := foo() _,y := foo() fmt.Println("x=", x) fmt.Println("y=", y) }
匿名变量不占命名空间,不会分配内存,所以匿名变量之间不存在重复声明。(在Lua等变编程语言里,匿名变量也被叫做哑元变量。)
注意事项:
函数外的每个语句都必须以关键字开始(var、const、func等)
- :=不能使用在函数外。
- _多用于占位,表示忽略值。
- go语言中变量声明必须使用,不适用就编译不过去
- 同一个作用域({})中不能重复声明同名的变量
总结:
变量声明的三种方式:
- var name1 string = "Hashflag"
- var name2 = "HashFlag"
- 函数内部专属:name3 := "HashFlag"
2、常量
相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。常量的声明和变量的声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。
const pi = 3.14159275453 const e = 2.7182
声明了pi e 这两个常量之后,在整个程序运行期间他们的值都不能再发生变化了。多个常量也可以一起声明:
const( pi = 3.14159275453 e = 2.7182 )
const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。例如:
const( n1 = 100 n2 n3 ) // n1,n2,n3的值都是100
3、iota
使用_跳过某些值
const( n1 = iote // 第一次默认为0 n2 // 1 _ n4 //3 )
iota声明中间插队
const( n1 = iota // 0 n2 = 100 // 100 n3 = iota // 2 n4 // 3 ) const n5 = iota // 0
定义数量级(这里的<<表示左移操作,1<<10表示将1的二进制向左移10位,也就是由1变成了10000000000,也就是十进制的1024。同理2<<2表示将2的二进制向左移2位,也就是由10变成了1000,也就是十进制的8。)
const( _ = iota KB = 1<<(10*iota) MB = 1<<(10*iota) GB = 1<<(10*iota) TB = 1<<(10*iota) PB = 1<<(10*iota) )
多个iota定义在一行
const( a, b = iota + 1, iota + 2 // 1,2 c, d // 2,3 e, f // 3,4 )
4、fmt格式化
package main // fmt占位符 func main(){ var n = 100 // 查看类型 fmt.Printf("%T\n", n) // 查看数据类型 fmt.Printf("%v\n", n) // 查看变量值 fmt.Printf("%b\n", n) // 二进制 fmt.Printf("%d\n", n) // 十进制 fmt.Printf("%o\n", n) // 八进制 fmt.Printf("%x\n", n) // 十六进制 var s = "word" fmt.Printf("%s\n", s) // 字符串 word 不会体现类型 fmt.Printf("%v\n", s) // 字符串 word 不会体现类型 fmt.Printf("%#v\n", s) // 字符串 "word" 体现出了具类型 }
二、Go的基本数据类型
Go语言中有丰富的数据类型,除了基本的整形、浮点型、布尔型、字符串外,还有数组、切片、结构体、函数、map、通道(channel)等。Go语言的基本数据类型和其它语言大同小异。
1、整形
基本整形
整形分为以下两个大类:
- 按长度分为:int8、int16、int32、int64
- 对应的无符号整形:uint8、uint16、uint32、uint64
其中,uint8是我们常用和熟知的byte型,int16对应C语言中的shot型(C:短整型),int64对应C语言中的long型(C:长整型)。
类型 | 描述 |
---|---|
uint8 | 无符号 8位整型 (0 到 255) |
uint16 | 无符号 16位整型 (0 到 65535) |
uint32 | 无符号 32位整型 (0 到 4294967295) |
uint64 | 无符号 64位整型 (0 到 18446744073709551615) |
int8 | 有符号 8位整型 (-128 到 127) |
int16 | 有符号 16位整型 (-32768 到 32767) |
int32 | 有符号 32位整型 (-2147483648 到 2147483647) |
int64 | 有符号 64位整型 (-9223372036854775808 到 9223372036854775807) |
特殊整形
类型 | 描述 |
---|---|
uint | 32位操作系统上就是uint32,64位操作系统上就是uint64 |
int | 32位操作系统上就是int32,64位操作系统上就是int64 |
uintptr | 无符号整型,用于存放一个指针 |
注意: 在使用int和 uint类型时,不能假定它是32位或64位的整型,而是考虑int和uint可能在不同平台上的差异。
注意事项:获取对象的长度的内建len()函数返回值的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或map的元素数量等都可以用int来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台的字节长度影响,不要使用int和uint。
数字字面量语法(Number literals syntax)
Go1.13版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字,例如:v:=0b001101101,代表二进制的101101,相当于十进制的45。v:=0o377,代表八进制的377,相当于十进制的255。v:=0x1p-2,代表十六进制的1除以22,也就是0.25。
package main import "fmt" func main(){ // 十进制 var a int = 10 fmt.Printf("%d \n", a) // 10 fmt.Printf("%b \n", a) // 1010 占位符%b表示二进制 // 八进制 以0开头 var b int = 077 fmt.Printf("%o \n", b) // 77 // 十六进制 以0x开头 var c int = 0xff fmt.Printf("%x \n", c) // ff fmt.Printf("%X \n", c) // FF }
2、浮点型
Go语言中有两种浮点型数:float32和float64。这两种浮点型数据格式遵循IEEE 754标准:flaot32的浮点最大范围约为1.8e308,可以使用一个常量定义:mathMaxFloat64。
打印浮点数时,可以使用fmt包配合动词%f,代码示例:
package main import ( "fmt" "math" ) func main() { fmt.Printf("%f\n", math.Pi) fmt.Printf("%.2f\n", math.Pi) // 浮点数 asd := 3.1415926 fmt.Printf("%T\n", asd) // 默认Go语言中的小数都是float64类型 asf := float32(3.14159) fmt.Printf("%T\n", asf) // 显示声明float32类型 // asd = asf // float32类型的值不能直接赋值给float64,反之也不行。 }
3、复数
两种数据类型complate64和complate128
package main import ( "fmt" "math" ) func main() { var c1 complex64 c1 = 1 + 2i var c2 complex128 c2 = 2 + 3i fmt.Println(c1) fmt.Println(c2) }
4、布尔值
Go语言中以bool类型进行声明布尔类型数据,布尔类型数据只有true(真)和falde(假)两个值。
注意:
- 布尔类型变量的默认值为false。
- Go语言中不允许将整形强制转换为布尔型。
- 布尔型无法参予数值运算,也无法与其它类型进行转换。
5、字符串
注:字符串默认不可修改
Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64等)一样。Go语言里的字符串的内部实现使用UTF-8编码。字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCII码字符,例如:
s1 := "hello" s2 := "你好"
字符串转义符
Go语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表格所示。
转义符 | 含义 |
---|---|
\r | 回车符(返回行首) |
\n | 换行符(直接跳到下一行的同列位置) |
\t | 制表符 |
\' | 单引号 |
\" | 双引号 |
\\ | 反斜杠 |
举个例子,我们要打印一个Windows平台下的一个文件路径:
package main import ( "fmt" ) func main() { str := "\"c:\\User\\AppDate\\.go\"" fmt.Println(str) // 双引号需要转换 fmt.Println(`c:\User\AppDate\.go\`) // 反引号原样输出 }
多行字符串
Go语言中要定义一个多行字符串时,就必须使用反引号字符:
package main import ( "fmt" ) func main() { asd := `第一行 第二行 第三行` fmt.Println(asd) }
反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
字符串的常用操作
方法 | 介绍 |
---|---|
len(str) | 求长度 |
+或fmt.Sprintf | 拼接字符串 |
strings.Split | 分割 |
strings.Contains | 判断是否包含 |
strings.HasPrefix,strings.HasSuffix | 前缀/后缀判断 |
strings.Index(),strings.LastIndex() | 子串出现的位置 |
strings.Join(a[]string, sep string) | join操作 |
具体示例:
package main import ( "fmt" "strings" ) func main() { strs1 := "Hello world!" fmt.Printf("%s %d\n", strs1, len(strs1)) strs2 := "Hi Griles!" fmt.Printf("%s\n", strs1+strs2) strs3 := fmt.Sprintf("%s%s\n", strs1, strs2) fmt.Printf("%s\n", strs3) strs4 := strings.Split(strs3, "i") fmt.Printf("%s\n", strs4) strs5 := strings.Contains(strs2, "i") fmt.Println(strs5) strs6 := strings.HasPrefix(strs2, "H") strs7 := strings.HasSuffix(strs2, "S") fmt.Println(strs6, strs7) strs8 := strings.Index(strs2, "Hi") strs9 := strings.LastIndex(strs2, "les") fmt.Println(strs8, strs9) strs10 := strings.Join([]string{"1", "2", "3"}, "+") fmt.Println(strs10) }
byte和rune类型
组成每个字符串的元素叫做“字符“,可以通过遍历或者单个获取字符串元素获得字符。字符用单引号('')包裹起来,像:
var a = '汉' var b = 'x'
Go语言的字符有以下两种:
- unit8类型,或者叫byte型,代表了ASCII码的一个字符。
- rune类型,代表一个utf-8字符。
当需要处理中文、日文或者其它符合字符时,需要用到rune类型。rune类型实际是一个int32。
Go使用了特殊的runne类型来处理Unicode,让基于Unicode的文本处理更为方便,也可以使用byte类型进行默认字符处理,性能和扩展性都有照顾。
示例:
// 遍历字符串 func traversalString() { s := "hello沙河" for i := 0; i < len(s); i++ { //byte fmt.Printf("%v(%c) ", s[i], s[i]) } fmt.Println() for _, r := range s { //rune fmt.Printf("%v(%c) ", r, r) } fmt.Println() }
输出:
104(h) 101(e) 108(l) 108(l) 111(o) 230(æ) 178(²) 153() 230(æ) 178(²) 179(³) 104(h) 101(e) 108(l) 108(l) 111(o) 27801(沙) 27827(河)
因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出的结果。
字符串底层是一个byte数组,所以可以和[]byte类型相互转换,字符串是不能修改的,字符串是由byte字节组成,所以字符串长度是byte字节的长度。rune类型用来表示utf8字符,一个rune字符由哟个或多个byte组成。
修改字符串
要修改字符串,需要先将其转换成[]runne或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。
package main import ( "fmt" ) // 浮点数 func main() { asd := "Hello world" dsa := "欸瑞克" fmt.Printf("asd: %T dsa:%T\n", asd, dsa) asd1 := []rune(asd) // 把字符串强制转换成一个rune切片 fmt.Printf("asd1: %T dsa:%d\n", asd1, asd1) fmt.Printf("asd1: %T dsa:%s\n", asd1, string(asd1)) // 把runne切片强制转换成字符串 asd1[0] = 'h' fmt.Printf("asd1: %T dsa:%d\n", asd1, asd1) fmt.Printf("asd1: %T dsa:%s\n", asd1, string(asd1)) asd1[0] = 'h' fmt.Printf("asd1: %d\n", asd1[0]) asd2 := "H" // string类型 asd3 := byte('H') // byte(uint8)类型 asd4 := "是" // string类型 asd5 := '是' // rune(int32)类型 fmt.Printf("%T %T %T %T\n", asd2, asd3, asd4, asd5) }
6、类型转换
Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。
强制类型转换的基本语法如下:
T(表达式)
其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等。
比如计算直角三角形的斜边长时使用math包的Sqrt()函数,该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制转换为float64类型。
func sqrtDemo() { var a, b = 3, 4 var c int // math.Sqrt()接收的参数是float64类型,需要强制转换 c = int(math.Sqrt(float64(a*a + b*b))) fmt.Println(c) }
三、运算符
运算符用于在程序运行时执行数学或逻辑运算。
Go 语言内置的运算符有:
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
算术运算符
运算符 | 描述 |
---|---|
+ | 相加 |
- | 相减 |
* | 相乘 |
/ | 相除 |
% | 求余 |
注意: ++(自增)和--(自减)在Go语言中是单独的语句,并不是运算符。
关系运算符
运算符 | 描述 |
---|---|
== | 检查两个值是否相等,如果相等返回 True 否则返回 False。 |
!= | 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 |
> | 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 |
>= | 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 |
< | 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 |
<= | 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 |
逻辑运算符
运算符 | 描述 |
---|---|
&& | 逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。 |
|| | 逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。 |
! | 逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。 |
位运算符
位运算符对整数在内存中的二进制位进行操作。
运算符 | 描述 |
---|---|
& | 参与运算的两数各对应的二进位相与。 (两位均为1才为1) |
| | 参与运算的两数各对应的二进位相或。 (两位有一个为1就为1) |
^ | 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 (两位不一样则为1) |
<< | 左移n位就是乘以2的n次方。 “a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。 |
>> | 右移n位就是除以2的n次方。 “a>>b”是把a的各二进位全部右移b位。 |
赋值运算符
运算符 | 描述 |
---|---|
= | 简单的赋值运算符,将一个表达式的值赋给一个左值 |
+= | 相加后再赋值 |
-= | 相减后再赋值 |
*= | 相乘后再赋值 |
/= | 相除后再赋值 |
%= | 求余后再赋值 |
<<= | 左移后赋值 |
>>= | 右移后赋值 |
&= | 按位与后赋值 |
|= | 按位或后赋值 |
^= | 按位异或后赋值 |
四、流程控制
Go语言中常用的流程控制有if和for,而switch和goto主要为了简化代码、降低重复代码而生成的结构,属于扩展类的流程控制。
注:Go语言中没有while循环,但是在Go语言中可以实现while的效果,通过for true来实现
1、if else(分支结构)
if条件判断的基本写法
Go语言中if条件判断的格式如下:
if 表达式1 { 分支1 } else if 表达式2 { 分支2 } else { 分支3 }
通过if进行判断,当表达式1的结果为true时执行分支1;否则继续进行else if判断表达式2结果为true时执行分支2;表达式2的结果为false时执行else;if判断中的逻辑是先进行if判断如果后面有else if 就进行else if判断,没有就往下进行else判断。具体情况具体分析,灵活判断。
Go语言规定if匹配的左括号{必须与if和表达式放在同一行,{放在其他位置会触发编码错误。同理,与else匹配的{和else if匹配的{都要在同一行。
if判断条件的特殊写法
if条件还有一种特殊的写发,可以在if表达式之前添加一个执行语句,再根据变量值进行判断,例如:
func demo() { if score := 5; score >= 8{ fmt.Println("A") }else if score > 6 && score { fmt.Println("B") }else { fmt.Println("C") } }
普通写法:
func demo() { score := 65 if score >= 90 { fmt.Println("A") } else if score > 75 && score < 90 { fmt.PrintLn("B") } else { fmt.Println("C")) } }
2、for循环结构
Go语言中的所有循环类型均可使用for关键字来完成。
for 循环的基本格式:
for 初始语句;条件表达式;结束语句{ 循环体语句 }
条件表达式返回true时循环体不停地进行循环,直到条件表达式返回false时自动退出循环。
func demo(){ for i := 0;i < 10; i++ { fmt.Println(i) } }
for循环的初始语句可以被忽略,但是初始语句后的分号必须要写:
func demo() { i := 0 for ; i < 10; i++ { fmt.Println(i) } }
for循环的初始语句和结束语句都可以省略:
func demo() { i := 0 for i < 10 { fmt.Println(i) i++ } }
这种写法类似于其它编程语言中的while,在while后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环
无限循环
for { 循环体语句 } 或 for true { 循环体语句 }
for循环可以通过break、goto、return、panic语句强制退出循环。
3、for range(键值循环)
Go语言中可以使用for range遍历数组、切片、字符串、map及通道(channel)。通过for range遍历的返回值有以下规律:
- 数组、切片、字符串返回索引和值。
- map返回键和值。
- 通道(channel)只返回通道内的值。
4、switch case
使用switch语句可方便地对大量数据值进行条件判断。
func demo() { finger := 3 switch finger { case 1: fmt.Println("猜错了") case 2: fmt.Println("猜少了") case 3: fmt.Println("回答正确") case 4: fmt.Println("猜多了") case 5: fmt.Println("猜错了") default: fmt.Println("无效的输入!") } }
Go语言规定每个switch只能有一个default分支。
一个分支可以有多个值,多个case值中间使用英文逗号分隔。
func demo() { switch n := 7; n{ case 1, 3, 5, 7, 9: fmt.Println("奇数") case 2, 4, 6, 8: fmt.Println("偶数") default: fmt.Println(n) } }
分支也可以使用表达式:
func demo() { age := 30 switch { case age < 25: fmt.Println("不及格!") case age > 25 && age < 35: fmt.Println("继续努力!") case age > 60: fmt.Println("及格") default: fmt.Println("良好") } }
fallthrough语法可以执行满足条件的case的下一个case,是为了兼容C语言中的case设计的。
func demo() { s := "a" switch { case s == "a": fmt.Println("a") fallthrough case s == "b": fmt.Println("b") case s == "c": fmt.Println("c") default: fmt.Println("...") } }
输出:
a b
5、goto(跳转到指定标签)
goto语句通过标签进行代码间的无条件跳转。goto语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto语句能简化一些代码的实现过程。例如双层嵌套的for循环要退出时:
func demo() { var breakFlag bool for i := 0; i < 10; i++ { if j := 0; j < 10; j++ { if j == 2 { // 设置推出标签 breakFlag = true break } fmt.Printf("%v-%v\n", i, j) } // 外层for循环判断 if breakFlag { break } } }
使用goto简化代码:
func demo() { for i := 0; i < 10; i++ { for j := 0; j < 10; j++ { if j == 2 { // 设置退出标签 goto breakTag } fmt.Printf("%v-%v\n", i, j) } } return // 标签 breakTag: fmt.Println("结束for循环") }
6、break(跳出循环)
break语句可以结束for、switch和select的代码块。
break语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的for、switch和select的代码块上。举个例子:
func demo() { BREAKDEMO: for i := 0; i < 10; i++ { for j := 0; j < 10; j++ { if j == 2 { break BREAKDEMO } fmt.Printf("%v-%v\n", i, j) } } fmt.Println("...") }
7、continue(继续下次循环)
continue语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for循环内使用。
在continue语句后添加标签时,表示开始标签对应的循环。例:
func demo() { forloop: for i := 0; i < 5; i++ { // forloop2: for j := 0; j < 5; j++ { if i == 2 && j == 2 { continue forloop } fmt.Printf("%v-%v\n", i, j) } } }
五、数组
Arrary(数组)
数组是同意种数据类型元素的集合。在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。基本语法:
// 定义一个长度为3的元素类型为int的数组a var a [3]int
1、数组定义
var 数组变量名[元素数量]T
注:数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。[5]int和[10]int是不同类型。
var a [3]int var b [4]int a = b // 不可以这样做,因为此时的a和b是不同的类型
数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会panic。
2、数组的初始化
数组的初始化方式有很多种
方式一
初始化数组时可以使用初始化列表来设置数组元素的值。
func main() { var testArray [3]int //数组会初始化为int类型的零值 var numArray = [3]int{1, 2} //使用指定的初始值完成初始化 var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化 fmt.Println(testArray) //[0 0 0] fmt.Println(numArray) //[1 2 0] fmt.Println(cityArray) //[北京 上海 深圳] }
方式二
除了每次确保提供初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,例如:
func main(){ var testArray [3]int var numArray = [...]int{1, 2} var cityArray = [...]string{"北京", "上海", "深圳"} fmt.Println(testArray) //[0 0 0] fmt.Println(numArray) //[1 2] fmt.Printf("type of numArray:%T\n", numArray) //type of numArray:[2]int fmt.Println(cityArray) //[北京 上海 深圳] fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string }
方式三
我们还可以使用指定索引值的方式来初始化数组,例如:
func main(){ a := [...]int{1: 1, 3: 5} fmt.Println(a) // [0 1 0 5] fmt.Printf("type of a:%T\n", a) //type of a:[4]int }
3、数组遍历
遍历数组a有以下两种方式:
方式一:for循环遍历
func main() { var a = [...]string{"北京", "上海", "深圳"} // 方法1:for循环遍历 for i := 0; i < len(a); i++ { fmt.Println(a[i]) } }
方式二:for range遍历
func main() { var a = [...]string{"北京", "上海", "深圳"} // 方法2:for range遍历 for index, value := range a { fmt.Println(index, value) } }
4、多维数组
Go语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。
二维数组的定义
func main(){ a := [3][2]string{ {"北京", "上海"}, {"广州", "深圳"}, {"成都", "重庆"}, } fmt.Println(a) // [[北京 上海] [广州 深圳] [成都 重庆]] fmt.Println(a[2][1]) // 支持索引取值:重庆 }
二维数组的遍历
func main(){ a := [3][2]string{ {"北京", "上海"}, {"广州", "深圳"}, {"成都", "重庆"}, } for _, v1 := range a { for _, v2 := range v1 { fmt.Printf("%s\t", v2) } fmt.Println() } }
输出:
北京 上海 广州 深圳 成都 重庆
注意:多维数组只有第一层可以使用...来让编译器推到数组长度。例如:
// 支持的写法 a := [...][2]string{ {"北京", "上海"}, {"广州", "深圳"}, {"成都", "重庆"}, } // 不支持多维数组的内层使用... b := [3][...]string{ {"北京", "上海"}, {"广州", "深圳"}, {"成都", "重庆"}, }
5、数组是值类型
数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
func modifyArray(x [3]int) { x[0] = 100 } func modifyArray2(x [3][2]int) { x[2][0] = 100 } func main() { a := [3]int{10, 20, 30} modifyArray(a) //在modify中修改的是a的副本x fmt.Println(a) //[10 20 30] b := [3][2]int{ {1, 1}, {1, 1}, {1, 1}, } modifyArray2(b) //在modify中修改的是b的副本x fmt.Println(b) //[[1 1] [1 1] [1 1]] }
注意:
- 数组支持“==”、"!="操作符,因为内存总是被初始化的。
- [n]*T表示指针数组,*[n]T表示数组指针。
六、切片
数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。例如:
func arraySum(x [3]int) int{ sum := 0 for _, v := range x{ sum = sum + v } return sum }
arraySum函数只能接收[3]int类型,其他的都不支持。比如:
a := [3]int{1, 2, 3}
数组a中已经有三个元素了,我们不能再继续往数组a中添加新元素。
1、切片的概念
切片(Slice)是一个拥有相同元素的可变长度的序列。它是基于数组类型做的一层封装。非常灵活,支持自动扩容。
切片是一个引用类型,它内部结构包含地址、长度和容量。切片一般用于快速的操作一块数据集合。
2、切片的定义
声明切片类型的基本语法:
var name []T
- name:表示变量名
- T:表示切片中的元素类型
例:
func main(){ // 声明切片类型 var a []string //声明一个字符串切片 var b = []int{} //声明一个整型切片并初始化 var c = []bool{false, true} //声明一个布尔切片并初始化 var d = []bool{false, true} //声明一个布尔切片并初始化 fmt.Println(a) //[] fmt.Println(b) //[] fmt.Println(c) //[false true] fmt.Println(a == nil) //true fmt.Println(b == nil) //false fmt.Println(c == nil) //false // fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和nil比较 }
切片的长度和容量
切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。
切片表达式
切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种是指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。
简单切片表达式
切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。切片表达式中的low和hight表示一个索引范围(左包含,右不包含),也就是下面代码中从数组a中选出1<=索引值<4的元素组成切片s,得到的切片长度=high-low,容量等于得到的切片的底层数组的容量。
func main() { a := [5]int{1, 2, 3, 4, 5} s := a[1:3] // s := a[low:high] fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s)) }
输出:
s:[2 3] len(s):2 cap(s):4
为了方便起见,可以省略切片表达式中的任何索引。省略了low则默认为0;省略了high则默认为切片操作数的长度:
a[2:] // 等同于 a[2:len(a)] a[:3] // 等同于 a[0:3] a[:] // 等同于 a[0:len(a)]
注意:
对于数组或字符串,如果0 <= low <= high <= len(a),则索引合法,否则就会索引越界(out of range)。
对切片在执行切片表达式时(切片再切片),high的上限边界是切片的容量cap(a),而不是长度。常量索引必须在有效范围内。如果low和high两个指标都是常量,它们必须满足low<=high。如果索引在运行时超出范围,就会发生运行时panic。
func main(){ a := [5]int{1, 2, 3, 4, 5} s := a[1:3] // s := a[low:high] fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s)) s2 := s[3:4] // 索引的上限是cap(s)而不是len(s) fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2)) }
输出:
s:[2 3] len(s):2 cap(s):4 s2:[5] len(s2):1 cap(s2):1
完整切片表达式
对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:
a[low : high : max]
上面的代码会构造与简单切片表达式a[low: high]想、相同类型、相同长度和元素的切片。另外,它会将得到的结果切片的容量设置为max-low。在完整切片表达式中只有第一个索引值(low)可以省略;它默认为0。
func main() { a := [5]int{1, 2, 3, 4, 5} t := a[1:3:5] fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t)) }
输出结果:
t:[2 3] len(t):2 cap(t):4
完整切片表达式需要满足的条件是0 <= low <= high <= max <= cap(a),其他条件和简单切片表达式相同。
使用make()函数构造切片
我们上面都是基于数组来创建的切片,如果需要动态创建一个切片,我们就需要使用内置的make()函数,格式如下:
make([]T, size, cap)
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量
例:
func main() { a := make([]int, 2, 10) fmt.Println(a) //[0 0] fmt.Println(len(a)) //2 fmt.Println(cap(a)) //10 }
main函数中a的内部存储空间已经分配了10个,但实际上只用了2个,容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量
切片的而本质
切片的本质就是随底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
判断切片是否为空
要检查切片是否为空,需要使用len(s)==0来判断,不要使用s == nil来判断。
3、切片不能直接比较
切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。切片唯一合法的比较是和nil比较。一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是不能说一个长度和容量都是0的切片一定是nil,因为切片初始化为空值的时候的长度和容量也都是0,但不等于nil
var s1 []int //len(s1)=0;cap(s1)=0;s1==nil s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
所以判断切片是否为空,要用len(s)==0来判断,不应该使用s == nil来判断
4、切片的赋值拷贝
拷贝前后两个变量共享底层数组,对一个切片的修改会影响零一个切片的内容,需要注意。
func main() { s1 := make([]int, 3) //[0 0 0] s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组 s2[0] = 100 fmt.Println(s1) //[100 0 0] fmt.Println(s2) //[100 0 0] }
5、切片遍历
切片的遍历方式和数组一样,支持索引比哪里和for range遍历。
func main() { s := []int{1, 3, 5} for i := 0; i < len(s); i++ { fmt.Println(i, s[i]) } for index, value := range s { fmt.Println(index, value) } }
6、append()方法为切片添加元素
Go语言的内建函数append()可以为切片动态添加元素。可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(否面加...)。
func main(){ var s []int s = append(s, 1) // [1] s = append(s, 2, 3, 4) // [1 2 3 4] s2 := []int{5, 6, 7} s = append(s, s2...) // [1 2 3 4 5 6 7] }
注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。
var s []int s = append(s, 1, 2, 3)
没有必要初始化一一个切片再传入append()函数使用。
s := []int{} // 没有必要初始化 s = append(s, 1, 2, 3) var s = make([]int) // 没有必要初始化 s = append(s, 1, 2, 3)
每个切片会指向一个底层数组,这个数组的容量够用就添加新元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
例:
func main() { //append()添加元素和切片扩容 var numSlice []int for i := 0; i < 10; i++ { numSlice = append(numSlice, i) fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice) } }
输出:
[0] len:1 cap:1 ptr:0xc0000a8000 [0 1] len:2 cap:2 ptr:0xc0000a8040 [0 1 2] len:3 cap:4 ptr:0xc0000b2020 [0 1 2 3] len:4 cap:4 ptr:0xc0000b2020 [0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000 [0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000 [0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000 [0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000 [0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000 [0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000
结论:
- append()函数将元素追加到切片的最后并返回该切片。
- 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。
append()函数支持一次性追加多个元素。
var citySlice []string // 追加一个元素 citySlice = append(citySlice, "北京") // 追加多个元素 citySlice = append(citySlice, "上海", "广州", "深圳") // 追加切片 a := []string{"成都", "重庆"} citySlice = append(citySlice, a...) fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆]
7、切片的扩容策略
可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:
newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { newcap += newcap / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } }
总结:
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
- 否则判断,如果旧切片的长的小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap).
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
注:
切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。
8、使用copy()函数赋值切片
func main() { a := []int{1, 2, 3, 4, 5} b := a fmt.Println(a) //[1 2 3 4 5] fmt.Println(b) //[1 2 3 4 5] b[0] = 1000 fmt.Println(a) //[1000 2 3 4 5] fmt.Println(b) //[1000 2 3 4 5] }
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。
Go语言内建的copy函数可以迅速地将一个切片的数据复制到另一个切片空间中,copy()函数的使用格式:
copy(destSlice, srcSlice []T)
总结:
- srcSlice:数据来源切片
- destSlice:目标切片
例:
func main() { // copy()复制切片 a := []int{1, 2, 3, 4, 5} c := make([]int, 5, 5) copy(c, a) //使用copy()函数将切片a中的元素复制到切片c fmt.Println(a) //[1 2 3 4 5] fmt.Println(c) //[1 2 3 4 5] c[0] = 1000 fmt.Println(a) //[1 2 3 4 5] fmt.Println(c) //[1000 2 3 4 5] }
9、从切片中删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
func main() { // 从切片中删除元素 a := []int{30, 31, 32, 33, 34, 35, 36, 37} // 要删除索引为2的元素 a = append(a[:2], a[3:]...) fmt.Println(a) //[30 31 33 34 35 36 37] }
删除元素:从切片a中删除索引为index的元素,操作方法是a=append(a[:index], a[index+1:]...)
七、Map
Go语言中提供的映射关系容器为map,其内部使用散列表(hash)实现。
map
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
map定义
Go语言中map的定义语法如下:
map[KeyType]ValueType
- KeyType:表示键的类型。
- ValueType:表示键对应的值的类型。
map类型的变量默认初始值为nil,需要使用make()函数来分配内存。
make(map[KeyType]ValueType, [cap])
cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
map基本使用
map中的数据都是成对出现的,map的基本使用示例:
func main() { scoreMap := make(map[string]int, 8) scoreMap["张三"] = 90 scoreMap["小明"] = 100 fmt.Println(scoreMap) fmt.Println(scoreMap["小明"]) fmt.Printf("type of a:%T\n", scoreMap) }
输出:
map[小明:100 张三:90] 100 type of a:map[string]int
map也支持在声明的时候填充元素,例:
func main() { userInfo := map[string]string{ "username": "沙河小王子", "password": "123456", } fmt.Println(userInfo) // }
判断某个键是否存在
Go语言中有个判断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("查无此人") } }
八、函数
函数是组织好的、可复用的、用于执行指定任务的代码块。
Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。
1、函数的定义
Go语言中定义函数使用func关键字,具体格式:
func 函数名(参数)(返回值){ 函数体 }
- 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名不能重名。
- 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用逗号,分隔。
- 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
- 函数体:实现指定功能的代码块。
例:
func intSum(x int, y int) int { return x + y }
函数的参数和返回值都是可选的,例如我们可以实现一个既不需要参数也没有返回值的函数:
func sayHellow() { fmt.Println("Hello 沙河") }
2、函数的调用
定义函数后,我们可以通过函数名()的方式调用函数。例:
func main() { sayHello() ret := intSun(10, 20) fmt.Println(ret) }
注意,调用有返回值的函数时,可以不接收其返回值。
3、参数
类型简写
函数的参数中如果相邻变量的类型相同,则可以省略类型,例:
func intSum(x, y int) int { return x + y }
intSum函数有两个参数,这两个参数的类型均为int,可以省略x的类型,因为y后面有和x一样的类型说明。
可变参数
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。
注:可变参数通常要作为函数的最后一个参数。
例:
func intSum(x ...int) int { fmt.Println(x) // x是一个切片 sun := 0 for _,v := range x { sum = sum + v } return sum }
调用上面的函数:
ret1 := intSum() ret2 := intSum(10) ret3 := intSum(10, 20) ret4 := intSum(10, 20, 30) fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60
固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,例:
func intSum(x int, y ...int) int { fmt.Print(x, y) sum := x for _, v := range y { sum = sum + v } return sum }
调用上述函数:
ret5 := intSum3(100) ret6 := intSum3(100, 10) ret7 := intSum3(100, 10, 20) ret8 := intSum3(100, 10, 20, 30) fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160
本质上,函数的可变参数是通过切片来实现的。
4、返回值
Go语言中通过return关键字向外输出返回值。
多返回值
Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。
func calc(x, y int) (int, int) { sum := x + y sub := x - y return sum, sub }
返回值命名
函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。
例:
func calc(x, y int) (sum, sub int){ sum = x+y sub = x-y return }
注:当一个返回值类型为slice时,nil可以看做是一个有效的slice,没有必要显示返回一个长度为0的切片。
func someFunc(x string) []int { if x == "" { return nil // 没有必要返回[]int } }
5、变量的作用域
全局变量
全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。在函数中可以访问到全局变量。
package main import "fmt" // 定义全局变量num var num int64 = 10 func testGlobalVar(){ fmt.Printf("num=%d\n", num) // 函数中可以访问全局变量num } func main() { testGlobalVar() // num=10 }
局部变量
局部变量又分为两种:函数内定义的变量无法在该函数外使用,例:main函数中无法使用testLocalVar函数中定义的变量x:
func testLocalVar(){ // 定义一个函数局部变量x,仅在该函数内部生效 var x int64 = 100 fmt.Print("x=%d\n", x) } func main() { testLocalCal() fmt.Println(x) // 此时无法使用变量x }
如果局部变量和全局变量重名,优先访问局部变量。
package main import "fmt" // 定义全局变量num var num int64 = 10 func testNum() { num := 100 fmt.Printf("num=%d\n", num) // 函数优先使用局部变量 } func main() { testNum() // num=100 }
场景:通常在if条件判断、for循环、switch语句上使用这种定义变量的方式。
func testLocalVar2(x, y int) { fmt.Println(x, y) // 函数的参数也是只在本函数中生效 if x > 0 { z := 100 // 变量z只在if语句块生效 fmt.Println(z) } // fmt.Println(z) // 此处无法使用变量z }
6、函数类型与变量
定义含数类型
可以使用type关键字来定义一个函数类型,例:
type calculation function(int, int) int
上面定义了一个calculation类型,它是一种数据类型,这种数据类型接收两个int类型的参数并且返回一个int类型的返回值。
也就是说,凡是发满足当前条件的calculation类型的函数,例如下面的add和sub是calculation类型。
func add(x, y int) int { return x + y } func sub(x, y int) int { return x - y }
add和sub都能赋值给calculation类型的变量。
var c calculation c = add
函数类型变量
可以声明函数类型的变量并为该变量赋值:
func main() { var c calculation // 声明一个calculation类型的变量c c = add // 把add赋值给c fmt.Printf("type of c:%T\n", c) // type of c:main.calculation fmt.Println(c(1,2)) // 像调用add一样调用c f := add // 将函数add赋值给变量f了 fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int fmt.Println(f(10,20)) // 像调用add一样调用f }
7、高阶函数
高阶函数分为函数作为参数和函数作为返回值两部分。
函数作为参数
函数可以作为参数:
func add(x, y int) int { return x + y } func calc(x, y int, op func(int, int) int) int { return op(x, y) } func main() { ret2 := calc(10, 20, add) fmt.Println(ret2) // 30 }
函数作为返回值
函数也可以作为返回值:
func do(s string) (func(int, int) int, error) { switch s{ case "+": return add, nil case "-": return sub, nil default: err := errors.New("无法识别的操作符") return nil, err } }
8、函数名和函数闭包
匿名函数
函数还可以作为返回值,但在Go语言中函数内部不能再像之前一样定义函数,只能定义匿名函数。匿名函数是没有函数名的函数,匿名函数定义格式:
func(参数)(返回值){ 函数体 }
匿名函数因为没有函数名,所以没有办法像普通函数一样进行调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
func main() { // 将匿名函数保存到变量 add := func(x, y int) { fmt.Println(x + y) } add(10,20) // 通过函数定义完加()直接执行 func(x, y int) { fmt.Println(x + y) }(10, 20) }
匿名函数多用于实现回调函数和闭包。
闭包
闭包是一个函数和与其相关的引用环境组合而成的实体。简单理解:闭包=函数+引用环境。
也可以这样理解:内函数引用了外函数参数,外函数返回了内函数。例:
func adder() func(int) int { var x int return func(y int) int { c += y return x } } func main() { var f= addr() fmt.Println(f(10)) // 10 fmt.Println(f(20)) // 30 fmt.Println(f(30)) // 60 f1 := adder() fmt.Println(f1(40)) // 40 fmt.Println(f1(50)) // 90 }
变量f是一个函数并且他引用了其外部作用域中的x变量,此时f就是一个闭包。在f的生命周期内,变量x也一直有效。闭包进阶,
例1:
func adder2(x int) func(int) int { return func(y int) int { x += y return x } } func main() { var f = adder2(10) fmt.Println(f(10)) // 20 fmt.Println(f(20)) // 40 fmt.Println(f(30)) // 70 f1 := adder2(20) fmt.Println(f1(40)) // 60 fmt.Println(f1(50)) // 110 }
例2:
func makeSuffixFunc(suffix string) func(string) string { return func(name string) string { if !strings.HasSuffix(name, suffix) { return name + suffix } return name } } func main() { jpgFunc := makeSuffixFunc(".jpg") txtFunc := makeSuffixFunc(".txt") fmt.Println(jpgFunc("test")) //test.jpg fmt.Println(txtFunc("test")) //test.txt }
例3:
func calc(base int) (func(int) int, func(int) int) { add := func(i int) int { base += i return base } sub := func(i int) int { base -= i return base } return add, sub } func main() { f1, f2 := calc(10) fmt.Println(f1(1), f2(2)) //11 9 fmt.Println(f1(3), f2(4)) //12 8 fmt.Println(f1(5), f2(6)) //13 7 }
9、defer语句
Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,
先被defer的语句最后被执行,最后被defer的语句,最先被执行。
例:
func main() { fmt.Println(1) fmt.Println(2) fmt.Println(3) fmt.Println("end") }
输出结果:
start end 3 2 1
由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
defer执行时机
在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。
第一步:返回值赋值 第二步:defer操作 第三步:return返回(RET指令)
10、内置函数介绍
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
panoc/recover
Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover模式来处理错误。panic可以放在任何地方引发,但recover只有在defer 调用的函数中有效。
例:
func funcA() { fmt.Println("func A") } func funcB() { panic("panic in B") } func funcC() { fmt.Println("func C") } func main() { funcA() funcB() funcC() }
输出:
func A panic: panic in B goroutine 1 [running]: main.funcB(...) .../code/func/main.go:12 main.main() .../code/func/main.go:20 +0x98
程序运行期间funcB中引发了panic导致程序崩溃,异常退出了。这个时候我们就可以通过recover将程序恢复回来,继续往后执行。
func funcA() { fmt.Println("func A") } func funcB() { defer func() { err := recover() //如果程序出出现了panic错误,可以通过recover恢复过来 if err != nil { fmt.Println("recover in B") } }() panic("panic in B") } func funcC() { fmt.Println("func C") } func main() { funcA() funcB() funcC() }
注意:
- recover()必须搭配defer使用。
- defer一定要在可能引发panic的语句之前定义。
九、指针
对于指针这个东西,学过C语言或C++语言的应该都了解,但是在Go中,指针的概念与C和C++中是不同的,Go语言中指针不能进行进行偏移和运算,是安全指针。
指针的三个基本概念:指针地址、指针类型和指针取值。
指针作用:指针是用来保存一个数据在内存中的地址的。
注:
Go语言中指针不能进行进行偏移和运算。
Go语言中的操作非常简单,只有两种形式两个符号
- &取地址符:作用取地址
- *****指针符:作用根据地址取值
1、指针地址和指针类型
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址操作”。Go语言中的值类型(int、float、bool、string、arrary、struct)都有对应的指针类型,如:*int、*int64、*string等。
取变量指针的语法:
ptr := &v // v的类型为T
其中:
- v:表示被取地址的变量,类型为T
- ptr:表示接收地址的变量,ptr的类型为*T,称为T的指针类型。*代表指针。
示例:
func main() { a := 10 b := &a fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078 fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int fmt.Println(&b) // 0xc00000e018 }
2、指针取值
在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,
示例:
func main() { // 指针取值 a := 10 b := &a // 取变量a的地址,将指针保存到b中 fmt.Printf("type of b:%T\n", b) c := *b // 指针取值(根据指针去内存取值) fmt.Printf("type of c:%T\n", c) fmt.Printf("value of c:%v\n", c) }
注:取地址操作符&和取值操作符是一对互补操作符,&取出地址,****根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性:
- 对变量进行取地址(&)操作,可以获得变量的指针变量。
- 指针变量的值是指针地址。
- 对指针地址进行取值(*)操作,可以获得指针指向的原变量的值。
示例:
func modify1(x int) { x = 100 } func modify2(x *int) { *x = 100 } func main() { a := 10 modify1(a) fmt.Println(a) // 10 只是把复制的值给改掉了,原值并没有变化 modify1(&a) fmt.Println(a) // 100 原值变了是因为直接改变了地址的值,所传参数也是地址 }
3、new和make
示例:
func main() { var a *int *a = 100 fmt.Println(*a) var b map[string]int b["asd"] = 100 fmt.Print(b) }
此代码会引发panic,在Go语言中对于引用类型的变量,在使用的时候不仅要声明,还要分配内存空间,否则值是没有办法进行存储的。对于值类型的声明不需要分配内存空间,因为他们在生命的时候已经默认分配好了内存空间。要分配内存,就涉及到new和make了。Go语言中new和make是内建的两个函数,主要用来分配内存。
panic原因:示例代码中*var a int 只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才能分配到内存空间,才可以进行赋值操作。正确写法示例:
func main() { var a * int a = new(int) *a = 10 fmt.Println(*a) }
示例中var b map[string]int只是声明变量b是一个map类型的变量,同理进行初始化址后才能进行赋值操作,示例:
func main() { var b map[string]int b = make(map[string]int, 10) b["asd"] = 100 fmt.Println(b) }
new
new是一个内置函数
函数签名:func new(Type) *Type
- Type表示类型,new函数只接收一个参数,这个参数是一个类型
- *Type表示类型指针,new函数返回一个指向该类的型内存地址的指针。
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。
示例:
func main() { a := new(int) b := new(bool) fmt.Printf("%T\n", a) // *int fmt.Printf("%T\n", b) // *bool fmt.Printf(*a) // 0 fmt.Printf(*b) // false }
make
make也是用于内存分配的,区别于new,它只能用于slice、map、和channel的内存创建,它返回的也是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回指针了。
make函数的函数签名:
func make(t type, size ...IntegerType) Type
make函数是不能替代的,在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对他们进行操作。
new和make的区别
- 二者都是用来做内存分配的;
- make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
- 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
十、结构体
Go语言中没有“类”这个概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
1、类别名和自定义类型
自定义类型
在Go语言中有一些基本的数据类型,string、整形、浮点型、布尔型、复数,Go语言中可以使用type关键字来定义自定义数据类型。
自定义类型,是定义了一个新的数据类型。可以基于内置的基本类型定义,也可以通过struct定义
示例:
// 将MyInt定义为int类型 type MyInt int
通过type关键字定义,MyInt是一种新的类型,它具有int的特性。
类型别名
类型别名是Go1.9版本添加的新功能。
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。
type TypeAlias = Type
rune和byte就是类型别名
type byte = unit8 type rune = int32
类型定义和类型别名的区别
类型别名与类型定义表面上只有一个等号的差异,结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
示例:
// 类型定义 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 }
2、结构体
Go语言中的基本数据类型可以表示一些事务的基本属性,但是当表达一个事物的全部或部分属性时,使用单一的基本数据类型是无法满足需求的,Go语言中提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫做结构体,英文名struct。通过struct可以定义自己的数据类型。
Go语言中通过struct实现面向对象。
2.1 结构体的定义
使用type和struct关键字来定义结构体
示例:
type 类型名 struct { 字段名 字段类型 字段名 字段类型 ... }
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:结构体字段的具体类型。
示例:
type person struct { name string city string age int8 }
同样类型的字段可以写在一行
type person struct { name, city string age int8 }
分析一下person这个结构体,它有三个字段,分别表示姓名、城市、和年龄。这就实现了和面向对象一样的效果。
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。本质上是一种聚合型的数据类型
2.2 结构体实例化
只有当结构体实例化时,才会真正的分配内存。注:结构体必须实例化才能使用
结构体是值类型,可以像声明内置类型一样使用var关键字声明结构体类型。
var 结构体实例 结构体类型
基本实例化
示例:
type person struct { name string city string age int8 } func main() { var p1 person p1.name = "eric" p1.city = "北京" pi.age = 18 fmt.Printf("p1=%v\n", p1) //p1={eric 北京 18} fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"eric", city:"北京", age:18} }
可以通过.来访问结构体的字段(成员变量),例如p1.name和p1.age。
匿名结构体
在定义一些临时结构等场景下还可以使用匿名结构体。
package main import ( "fmt" ) func () { var user struct{Name string; Age int} user.Name = "eric" user.Age = 18 fmt.Printf("%#v\n", user) }
创建指针类型结构体
可以使用new关键字对结构体进行实例化,得到的是结构体的地址。
var p2 = new(person) fmt.Printf("%T\n", p2) //*main.person fmt.Printf("p2=%#v\n", p2) //p2=&main.Person{name:"", city:"", ahe:0}
从打印的数据类型中显示p2是一个结构体指针。
Go语言中支持对结构体指针直接使用.访问结构体成员。
var p2 = new(person) p2.name = "eric" p2.age = 18 p2.city = "上海" fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"eric", city:"上海", age: 18}
取结构体的地址实例化
使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
p3 := &person() fmt.Printf("%T\n", p3) //*main.person fmt.Printf("p3=%#v\n") // p3=&main.person{name:"", city:"", age:0} p3.name = "eric" p3.age = 18 p3.city = "成都" fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"eric", city:"成都", age:18}
p3.name="eric"这种写法是Go语言中的语法糖,标准写法是(*p3).name = "eric"
2.3 结构体初始化
未初始化的结构体,它的成员变量都是对应类型的零值。
type person struct { name string city string age int8 } func main() { var p4 person fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0} }
使用键值对方式初始化
使用键值对对话结构体进行初始化时,见对应结构体的字段,值对应该字段的初始值。
结构体键值对结构体初始化示例:
p5 := person{ name: "eric", city: "北京", age: 18, } fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"eric", city:"北京", age:18}
结构体指针键值对初始化示例:
p6 := &person{ name: "eric", city: "成都", age: 18, } fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"eric", city:"成都", age: 18}
注:当某些字段没有初始值的时候,该字段可以不写,没有指定的初始值的字段的值就是该字段类型的零值。
p7 := &person{ city: "成都", } fmt.Print("p7=%#v\n", p7) //p7=&main.person{name:"", city:"成都", age:0}
使用值的方式初始化
初始化结构体有简写方法,在初始化的时候不写键,直接写值
示例:
p8 := &person{ "eric", "成都", 18, } fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"eric", city:"成都", age:18}
注:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 此方式不能和键值初始化方式混用。
2.4 结构体内存布局
结构体占用一块连续的内存
package main import "fmt" func main() { type test struct { a int8 b int8 c int8 d int8 } n := test{ 1, 2, 3, 4, } fmt.Printf("n.a %p\n", &n.a) fmt.Printf("n.b %p\n", &n.b) fmt.Printf("n.c %p\n", &n.c) fmt.Printf("n.d %p\n", &n.d) }
结果:
n.a 0xc0000a2058 n.b 0xc0000a2059 n.c 0xc0000a205a n.d 0xc0000a205b
里面的a和b是指10和11,0x代表是16进制
空结构体
空结构体不占用内存空间
var v struct{} fmt.Print(unsafe.Sizeof(v)) //0
或
type person struct{} var p person fmt.Print(unsafe.Sizeof(p)) //0
或
type person struct{} p := person{} fmt.Print(unsafe.Sizeof(p)) //0
3、构造函数
Go语言的结构体没有构造函数,但可以实现。
示例:
func newPerson(name, city string, age int8) *person { return &person{ name: name, city: city, age: age, } }
调用构造函数示例:
p9 := newPerson("eric", "成都", 18) fmt.Printf("%#v\n", p9) //&main.person{name:"eric", "成都", age:18}
4、方法和接收者
Go语言中的方法(Method)是一种用于特定类型变量的函数。这种特定类型变量叫接收者(Receiver)。接收者的概念类似于其它语言中的this或者self。
定义方式:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 }
- 接收者变量:接收者中的参数变量在命名时,官方建议使用接收者类型名称首字母的小写,而不是self或this等命名。例:Dog 命名为 d
- 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
示例:
//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("eric", 18) p1.Dream() }
注:方法与函数的区别是,函数不属于任何类型,方法属于特定类型。
值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
//SetAge 设置p的年龄 //使用值接收者 func (p Person) SetAge2(newAge int8) { p.age = newAge } func main() { p1 := NewPerson("eric", 18) p1.Dream() fmt.Println(p1.age) // 18 p1.SetAge2(20) // (*p1).SetAge2(20) fmt.Println(p1.age) // 18 }
指针类型的接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,也就是直接修改地址中对应的值。这种方式类似于其它语言的this或self
//SetAge 设置p的年龄 //使用指针接收者 func (p *Person) SetAge(newAge int8) { p.age = newAge }
调用方法示例:
func main() { p1 := NewPerson("eric", 18) fmt.Println(p1.age) // 18 p1.SetAge(20) fmt.Println(p1.age) }
指针类型接收者的使用场景
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其它的方法也应该使用指针接收者。
5、任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
示例:
//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 }
注:非本地类型不能定义方法,也就是说不能给别的包的类型定义方法。
6、结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
//Person 结构体Perdson类型 type Person struct { string int } func main() { p1 := Person{ "eric", 18, } fmt.Printf("%#v\n", p1) //main.Person{string:"北京", int:18} fmt.Println(p1.string, p1.int) //北京 18 }
注:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
7、嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
示例:
//Address 地址结构体 type Address struct { Province string City string } //User 用户结构体 type User struct { Name string Gender string Address Address } func main() { user1 := User{ Name: "eric", Gender: "男", Address: Address{ Province: "成都", City: "简阳", }, } fmt.Printf("user1=%#v\n", user1) //user1=main.User{Name:"eric", Gender:"男", Address:main.Address{Province:"成都", City:"简阳"}} }
嵌套匿名字段
嵌套的Address结构体也可以采用匿名字段的方式
示例:
//Address 地址结构体 type Address struct { Province string City string } //User 用户结构体 type User struct { Name string Gender string Address //匿名字段 } func main() { var user2 User user2.Name = "eric" user2.Gender = "男" user2.Address.Province = "成都" //匿名字段默认使用类型名作为字段名 user2.City = "简阳" //匿名字段可以省略 fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"eric", 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 Email } func main() { var user3 User user3.Name = "eric" user3.Gender = "男" // user3.CreateTime = "2019" //ambigous selector user3.CreateTime user3.Address.CreateTime = "2020" // 指定Address结构体中的CreateTime user3.Email.CreateTime = "2020" // 指定Email结构体中的CreateTime }
8、结构体的“继承”
Go语言中使用结构体可以实现面向对象中的继承。
示例:
//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: "eric", } } d1.wang() //eric会汪汪~ d1.move() //eric会动! }
9、结构体字段的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅定义当前结构体的包中可访问)。
10、结构体与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.Print("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) }
11、结构体标签(Tag)
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag在结构体字段的后方定义,由一对反引号包裹起来,具体格式:
`key1:"value1" key2:"value2"`
结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
注:为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如:不要在key和value之间加空格。
例如我们为Student结构体的每个字段定义json序列化时使用的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: "eric", } 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":"男"} }
12、结构体和方法补充知识点
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意。
示例:
type Person struct { name string age int8 dreams []string } func (p *Person) SetDreams(dreams []string) { p.dreams = dreams } func main() { p1 := Person{name: "eric", age: 18} data := []string{"吃饭", "睡觉", "打豆豆"} p1.SetDreams(data) // 你真的想要修改 p1.dreams 吗? data[1] = "不睡觉" fmt.Println(p1.dreams) // ? }
正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。
func (p *Person) SetDreams(dreams []string) { p.dreams = make([]string, len(dreams)) copy(p.dreams, dreams) }
同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。
十一、接口
接口(interface)定义了一个对象的行为规范,只定义规范不实现,具体的对象来实现规范性的细节。
1、接口类型
在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface是一组method的集合,是duck-type programming的一种体现。接口做的事就是定义一个协议(规则),并不关心属性(数据),只关心行为(方法).
注:接口(interface)是一种类型。
接口的概念:接口是一组方法签名的集合,可以通过定义一个结构体实现该接口的所有方法。(接口定义了对象的行为)
在Go语言中没有明确提到一个类型是否实现了一个接口,如果一个类型实现了在接口中定义的签名方法,则称该类型实现了该接口
2、接口的定义
Go语言提倡面向接口编程。
每个接口由数个方法组成,接口定义格式:
type 接口类型名 interface{ 方法名1(参数列表1) 返回值列表1 方法名2(参数列表2) 返回值列表2 ... }
- 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫writer,有字符串功能的接口叫Stringer等。接口名最好突出该接口定义的类型含义。
- 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量可以省略。
示例:
type Person interface{ eat([]byte) error }
3、实现接口的条件
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换而言之:接口就是一个需要实现的方法列表。
定义一个接口:
// Sayer 接口 type Sayer interface { say() }
定义dog和cat两个结构体:
type dog struct {} type cat struct {}
因为Sayer接口里只有一个say方法,所以下只需要给dog和cat分别实现say方法就可以实现Sayer接口了。
// dog实现了Sayer接口 func (dog) say() { fmt.Println("汪汪汪") } // cat实现Sayer接口 func(c cat) asy() { fmt.Println("喵喵喵") }
接口的实现:只要实现了接口中的所有方法就实现了这个接口
4、接口类型变量
接口类型变量能够存储实现了该接口的实例,上面的Sayer类型的变量能够存储dog和cat类型的变量。
func () { var x Sayer // 声明一个Sayer类型的变量x a := cat() // 实例化一个cat b := dog() // 实例化一个dog x = a // 可以把cat实例直接赋值给x x.say() // 喵喵喵 x = b // 可以把dog实例直接赋值给x x.say() // 汪汪汪 }
Tips:_的使用
// 摘自gin框架routergroup.go type IRouter interface {...} type RouterGroup struct {...} var _IRouter = &RouterGroup{} // 确保RouterGroup实现了接口IRouter
5、值接收者和指针接收者实现接口的区别
使用值接收者实现接口和使用指针接收者实现接口的区别:
示例:Mover和dog两个结构体
type Mover interfance { move() } type dog struct{}
值接收者实现接口
func(d dog) move() { fmt.Println("狗会动") }
此时实现接口的是dog类型:
func main() { var x Mover var wangcai = dog{} // 旺财是dog类型 x = wangcai // x可以接收dog类型 var fugui = &dog{} // 富贵是*dog类型 x = fugui // x可以接收*dog类型 x.move() }
使用值接收者实现接口后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给接口变量。因为Go语言中有针对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui。
指针接收者实现接口
示例:
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类型 }
此时实现Mover的接口是dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。
6、类型与接口的关系
一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。
示例:
// Sayer 接口 type Sayer interfance { say() } // Mover 接口 type Mover interfance { move() }
创建一个dog结构体,既可以实现Sayer接口,也可以实现Mover接口。
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() }
多个类型实现同一接口
Go语言中不同的类型还可以实现同一接口。
示例:
// Mover 接口 type Mover interfance { move() }
创建两个结构体dog和car,都实现Mover接口
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) }
这个时候就可以忽略他们具体是什么,只需要调用他们的move方法就可以。
func main() { var x Mover var a = dog{name: "旺财"} var b = car{brand: "保时捷"} x = a x.move() x = b x.move() }
注:一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
示例:
// WashingMachine 洗衣机 type WashingMachine interfance { wash() dry() } // 甩干器 type dryer struct{} // 实现WsahingMachine接口的dry()方法 func (d dryer) dry() { fmt.Println("甩一甩") } // 海尔洗衣机 type haier struct { dryer //嵌入甩干器 } // 实现WashingMachine接口的wash()方法 func (h haier) wash() { fmt.Println("洗刷刷") }
7、接口嵌套
接口与接口间可以通过嵌套创造出新的接口。
// Sayer 接口 type Sayer interfance { say() } // Mover 接口 type Mober interfance { move() } // 接口嵌套 type animal interfance { 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() }
8、空接口
空接口的定义
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
func mian() { // 定义一个空接口x var x = interfance{} s := "Hello 沙河" x = s fmt.Printf("type:%T value:%v\n", x, x) i := 100 x = i fmt.Print("type:%T value:%v\n", x, x) b := true x = b fmt.Printf("type:%T value:%v\n", x, x) }
接口的应用
空接口作为函数的参数
使用空接口实现可以接收任意类型的的函数参数。
// 空接口作为函数参数 func show(a interfance{}) { fmt.Printf("type:%T value:%v\n", a, a) }
空接口作为map的值
使用空接口实现可以保存任意值的字典。
// 空接口作为map值 var studentInfo = make(map[string]interfance{}) studentInfo["name"] = "Eric" studentInfo["age"] = 18 studentInfo["married"] = false fmt.Println(studentInfo)
9、类型断言
空接口可以存储任意类型的值,获取空接口中存储的具体数据。
接口值
一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成。这两部分称为接口的动态类型和动态值。
示例:
var w io.Writer w = os.Stdout w = new(bytes.Buffer) w = nil
判断空接口中的值使用断言:
断言语法格式:
x.(T)
- x:表示类型为interface{}的变量。
- T:表示断言x可能是的类型。
改语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false在表示断言失败。
示例:
func main() { var x interfance{} x = "Hello 沙河" v, ok := x.(string) if ok { fmt.Println(v) } else { fmt.Println("类型断言失败") } }
如果需要断言多次就需要写多个if判断, 这时可以使用switch语句来实现:
func justifyType(x interfance{}) { 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.Printf("unsupport type!") } }
空接口有可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
注意:只有当两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
十二、包
在工程化的Go语言开发项目中,Go语言的源码复用是建立在包(package)基础之上的。
1、Go语言的包介绍
包(package)是多个Go源码的集合,是一种高级的代码复用方案,Go语言为我们提供了很多内志保,如fmt、os、io等。
2、定义包
我们可以根据自己需要创建自己的包。一个包可以简单理解为一个存放.go文件的文件夹。该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。
package 包名
注意:
- 一个文件夹下面直接包含的文件只能归属一个package,同样一个package的文件不能在多个文件夹下
- 报名可以不和文件夹的名字一样,包名不能包含 - 符号。
- 包名为main的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main包的源代码则不会得到可执行文件。
3、可见性
如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(pubilc)。在Go语言中只需要将标识符的首字母大写就可以让标识符对外可见了。
示例:
package pkg2 import "fmt" // 包变量的可见性 var a = 100 // 首字母小写,外部包不可见,只能在当前包内使用 // 首字母大写外部包可见,可在其他包中使用 const Mode = 1 type person struct { // 首字母小写,外部包不可见,只能在当前包内使用 name string } // 首字母大写,外部包可见,可在其他包中使用 func Add(x, y int) int { return x + y } func age() { // 首字母小写,外部包不可见,只能在当前包内使用 var Age = 18 // 函数局部变量,外部包不可见,只能当函数内使用 fmt.Println(Age) }
结构体中的字段名和接口中的方法名如果首字母都是大写,外部包可以访问这些字段和方法。
示例:
type nStudent struct { Name string //可在包外访问的方法 class string //仅限包内访问 } type Payer interfance { init() //仅限包内访问的方法 Pay() //可在包外访问的方法 }
4、包的导入
要在代码中引入其他包的内容,需要使用import关键字导入使用的包。
具体语法:
import ”包的路径“
注意:
- import导入语句通常放在文件开头包声明的语句下面。
- 导入的包名需要使用双引号包裹起来。
- 包明是从$GOPATH/src/后开始计算的,使用/进行路径分隔。
- Go语言中禁止循环导入包。
单行导入
单行导入包的格式:
import "package1" import "package2"
多行导入
多行导入包的格式:
import ( "package1" "package2" )
5、自定义包名
在导入报名的时候,我们还可以为导入的包设置别名。通常用于导入的包名太长或者导入的包名冲突的情况。
具体语法:
import 别名 "包的路径"
单行导入方式定义别名:
import "fmt" import m "github.com/pcd/studygo/pkg_test" func main() { fmt.Println(m.Add(100, 200)) fmt.Println(m.Mode) }
多行导入方式定义别名:
import ( "fmt" m "github.com/pcd/studygo/pkg_test" ) func main() { fmt.Println(m.Add(100,200)) fmt.Println(m.Mode) }
6、匿名导入包
如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包。
使用格式:
import _ "包的路径"
匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中
7、init()初始化函数
init()函数介绍
在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。需要注意的是:init()函数没有参数也没有返回值。init()函数在程序运行时自动被调用执行,不能在代码中主动调用它。