《Go语言开发实战》笔记
1 基础部分
Go语言基本结构及说明
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("hello world")
// 查看版本
fmt.Println(runtime.Version())
}
func main()是程序入口。所有Go函数以关键字func开头,每一个可执行程序都必须包含main()函数,通常是程序启动后第一个执行的函数,如果有init()函数则会先执行init()函数。
除此之外,还有以下几点值得注意。
- (1)只有package名称为main的包可以包含main()函数。
- (2)一个可执行程序有且仅有一个main包。
- (3)通过import关键字来导入其他非main包。
- (4)可以通过import关键字单个导入,也可以同时导入多个。
2 基本语法
2.1 变量
变量的本质是计算机分配的一小块内存,专门用于存放指定数据,在程序运行过程中该数值可以发生改变;变量的存储往往具有瞬时性,或者说是临时存储,当程序运行结束,存放该数据的内存就会释放,该变量就会随着内存的释放而消失。就像日常生活中存放水的水杯,当水杯损坏的时候,装在里面的水也会流失掉。
变量又分为局部变量和全局变量。
- 局部变量,是定义在大括号({})内部的变量,大括号的内部也是局部变量的作用域。
- 全局变量,是定义在函数和大括号({})外部的变量。
Go 语言的变量名由字母、数字、下画线组成,首个字符不能为数字;Go 语法规定,定义的局部变量若没有被调用会发生编译错误。
变量声明
未初始化变量的默认值有如下特点:
- 整型和浮点型变量默认值:0。
- 字符串默认值为空字符串。
- 布尔型默认值为false。
- 函数、指针变量、切片默认值为nil。
初始化变量的标准格式如下:
var a int = 199 //初始化变量的标准格式
var b = 100 //初始化变量的编译器自动推断类型格式
c := 123 //初始化变量的简短声明格式
使用 := 赋值操作符可以高效地创建一个新的变量,称为初始化声明。声明语句省略了 var 关键字,变量类型将由编译器自动推断。这是声明变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。该变量名必须是没有定义过的变量,若定义过,将发生编译错误。
package main
func main(){
// 这样写是错误的
var a = 123
a := 222
}
./variable.go:7:4: no new variables on left side of :=
多个短变量声明和赋值中,至少有一个新声明的变量出现在左侧,那么即便其他变量名可能是重复声明的,编译器也不会报错。情况如下所示:
package main
import "fmt"
func main(){
// 这种情况不会报错
var a = 666
a, b := 1,2
fmt.Println("a>>>",a) // 1
fmt.Println("n>>>",b) // 2
}
虽然这种方法不会报错,但是在使用过程中应尽量避免。
变量多重赋值
变量多重赋值是指多个变量同时赋值。
Go语法中,变量初始化和变量赋值是两个不同的概念。Go语言的变量赋值与其他语言一样,但是Go提供了其他程序员期待已久的多重赋值功能,可以实现变量交换。多重赋值让Go语言比其他语言减少了代码量。
go语言中使用多重赋值进行变量交换的例子:
package main
import "fmt"
func main(){
var a = 666
a, b := 111,222
// 变量交换
b, a = a, b
fmt.Println("a>>>",a) // 222
fmt.Println("n>>>",b) // 111
}
需要注意的是,多重赋值时,左值和右值按照从左到右的顺序赋值。这种方法在错误处理和函数当中会大量使用。
匿名变量
Go语言的函数可以返回多个值,而事实上并不是所有的返回值都用得上。那么就可以使用匿名变量,用下画线“_”替换即可。
例如,定义一个函数,功能为返回两个int型变量,第一个返回10,第二个返回20,第一次调用舍弃第二个返回值,第二次调用舍弃第一个返回值,具体语法格式如下所示。
package main
import "fmt"
func getData()(int, string, bool){
return 666,"whw",false
}
func main(){
var a = 666
a, b, _ := getData()
fmt.Println("a>>>",a) // 666
fmt.Println("n>>>",b) // whw
}
匿名变量既不占用命名空间,也不会分配内存。
2.2 数据类型
在Go语言中,有以下几种数据类型:
基本数据类型(原生数据类型):整型、浮点型、复数型、布尔型、字符串、字符(byte、rune)。
复合数据类型(派生数据类型):数组(array)、切片(slice)、映射(map)、函数(function)、结构体(struct)、通道(channel)、接口(interface)、指针(pointer)。
整型
整型分两大类。有符号整型:int8、int16、int32、int64、int。
无符号整型:uint8、uint16、uint32、uint64、uint。
其中uint8就是byte型,int16对应C语言的short型,int64对应C语言的long型。
![image-20201022114615738](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022114615738.png)
浮点型
![image-20201022114652127](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022114652127.png)
常量math.MaxFloat32表示float32能获取的最大值,大约是3.4×1038;常量math.SmallestNonzeroFloat32表示float32能获取的最小值,大约为1.4×10-45。
常量math.MaxFloat64表示float64能获取的最大值,大约是1.8×10308;常量math.SmallestNonzeroFloat64表示float64能获取的最小值,大约为4.9×10-324。
复数型
复数型用于表示数学中的复数,如1+2j、1-2j、-1-2j等。关于复数型的说明,如表2.3所示。
![image-20201022115103164](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022115103164.png)
布尔型
布尔型用预定义标识符bool表示。在C语言中,对于布尔型的值定义,非0表示真,0表示假。而在Go语言中,布尔型的值只可以是常量true或者false。
声明方式如下所示。
var flag bool
布尔型无法参与数值运算,也无法与其他类型进行转换。
字符串
字符串在Go语言中是以基本数据类型出现的,使用字符串就像使用其他原生基本数据类型int、float32、float64、bool一样。
字符串在C++语言中,以类的方式进行封装,不属于基本数据类型。
在go中使用字符串:
var s1 string //定义名为s1的字符串类型变量
s1 = "HelloWorld" //变量赋值
student1 := "火影whw" //以自动推断方式初始化
有些字符串没有现成的文字代号,所以只能用转义字符来表示。常用的转义字符如表2.4所示:
![image-20201022115523155](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022115523155.png)
定义多行字符串的方法如下:
- 双引号书写字符串被称为字符串字面量(string literal),这种字面量不能跨行。
- 多行字符串需要使用反引号“`”,多用于内嵌源码和内嵌数据。
- 在反引号中的所有代码不会被编译器识别,而只是作为字符串的一部分。
- 多行字符串定义方式如例2-1所示。
package main
import "fmt"
func getString()(string){
s1 := `
x := 123
y := 666
ss := "A Hero's Country!"
`
return s1
}
func main(){
a := getString()
fmt.Println("a>>>",a)
}
字符
字符串中的每一个元素叫作“字符”,定义字符时使用单引号。Go语言的字符有两种,如表2.5所示。
![image-20201022120100841](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022120100841.png)
声明示例如下(注意必须是单引号!):
package main
import "fmt"
func main(){
// 注意必须是单引号!!!
var a byte = 's'
var b rune = '王'
fmt.Println("a>>>",a) // 115
fmt.Println("b>>>",b) // 29579
}
2.3 打印格式化
打印格式化通常使用fmt包,通用的打印格式如表:
![image-20201024100236018](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024100236018.png)
具体的使用方法:
package main
import "fmt"
func main(){
str := "wanghw"
fmt.Printf("%T, %v \n",str,str) //string, wanghw
var a rune = '王'
fmt.Printf("%T, %v \n",a,a) //int32, 29579
var b byte = 'b'
fmt.Printf("%T, %v \n",b,b) //uint8, 98
var c int32 = 123
fmt.Printf("%T, %v \n",c,c) //int32, 123
}
通过上例可以看出,使用通用的格式打印,输出的结果可能不是自己想要的,为了确保输出结果与需求一致,还需要学习具体格式的打印方式。
布尔型打印格式
![image-20201024100347282](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024100347282.png)
package main
import "fmt"
func main(){
var flag bool
// flag默认是false
fmt.Printf("%T, %t \n",flag,flag) //bool, false
flag = true
fmt.Printf("%T, %t \n",flag,flag) //bool, true
}
整型打印格式
![image-20201024100705972](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024100705972.png)
package main
import "fmt"
func main(){
fmt.Printf("%T, %d \n",123, 123) //int, 123
fmt.Printf("%T, %5d \n",123, 123) //int, 123
fmt.Printf("%T, %05d \n",123, 123) //int, 00123
fmt.Printf("%T, %b \n",123, 123) //int, 1111011
fmt.Printf("%T, %o \n",123, 123) //int, 173
fmt.Printf("%T, %c \n",98, 98) //int, b
fmt.Printf("%T, %q \n",98, 98) //int, 'b'
fmt.Printf("%T, %x \n",123, 123) //int, 7b
fmt.Printf("%T, %X \n",123, 123) //int, 7B
fmt.Printf("%T, %U \n",'王', '王') //int32, U+738B
}
浮点型与复数型打印格式
![image-20201024101359915](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024101359915.png)
package main
import "fmt"
func main(){
fmt.Printf("%b \n",123.23433) //8671845041675824p-46
fmt.Printf("%f \n",123.2) //123.200000
fmt.Printf("%.2f \n",123.22222) //123.22
fmt.Printf("%e \n",123.22222) //1.232222e+02
fmt.Printf("%E \n",123.22222) //1.232222E+02
fmt.Printf("%.1e \n",123.22222) //1.2e+02
fmt.Printf("%F \n",123.22222) //123.222220
fmt.Printf("%g \n",123.22222) //123.22222
fmt.Printf("%G \n",123.22222) //123.22222
}
关于复数的打印格式如下
package main
import "fmt"
func main(){
var value complex64 = 2.2 + 22i
value2 := complex(2.2,222)
fmt.Println(real(value)) //2.2
fmt.Println(imag(value)) //22
fmt.Println(value2) //(2.2+222i)
}
字符串打印与字节数组的打印格式
![image-20201024102229661](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024102229661.png)
package main
import "fmt"
func main(){
arr := []byte{'a','b','c','d'}
fmt.Printf("%s \n","火影whw")//火影whw
fmt.Printf("%q \n","火影whw")//"火影whw"
fmt.Printf("%x \n","火影whw")//e781abe5bdb1776877
fmt.Printf("%X \n","火影whw")//E781ABE5BDB1776877
fmt.Printf("%T, %s \n",arr, arr)//[]uint8, abcd
fmt.Printf("%T, %q \n",arr, arr)//[]uint8, "abcd"
fmt.Printf("%T, %x \n",arr, arr)//[]uint8, 61626364
fmt.Printf("%T, %X \n",arr, arr)//[]uint8, 61626364
}
2.4 数据类型转换
Go语言采用数据类型前置加括号的方式进行类型转换,格式如:T(表达式)。T表示要转换的类型;表达式包括变量、数值、函数返回值等。
类型转换时,需要考虑两种类型之间的关系和范围,是否会发生数值截断。就像将1000毫升的水倒入容积为500毫升的瓶子里,余出来500毫升的水便会流失。值得注意的是,布尔型无法与其他类型进行转换。
package main
import "fmt"
func main(){
a := 100
b1 := float64(a)
b2 := string(a)
fmt.Println("b1>>>",b1)
fmt.Println("b2>>>",b2)
}
浮点型与整型之间转换
float和int的类型精度不同,使用时需要注意float转int时精度的损失。
package main
import "fmt"
func main() {
chinese := 90
english := 98.9
avg1 := (chinese + int(english)) / 2
avg2 := (float64(chinese) + english) / 2
fmt.Println("avg1>>>",avg1) // 94
fmt.Println("avg2>>>",avg2) // 94.45
}
整型转字符串类型
这种类型的转换,其实相当于byte或rune转string。
int数值是ASCII码的编号或unicode字符集的编号,转成string就是根据字符集,将对应编号的字符查找出来。当该数值超出unicode编号范围,则转成的字符串显示为乱码。例如,19968转string,就是“一”。
备注:
- ASCII字符集中数字的十进制范围是48~57;
- ASCII字符集中大写字母的十进制范围是65~90;
- ASCII字符集中小写字母的十进制范围是97~122;
- unicode字符集中汉字的范围是4e00~9fa5,十进制范围是19968~40869。
详情如下:
![image-20201022140218438](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022140218438.png)
具体的使用方法如下:
package main
import "fmt"
func main() {
a := 97
x := 19969
ret1 := string(a)
ret2 := string(x)
fmt.Println("ret1>>>",ret1) // a
fmt.Println("ret2>>>",ret2) // 丁
}
Go语言中不允许字符串转int
package main
import "fmt"
func main() {
a := "丁"
// 不能将字符串转为int
ret1 := int64(a)
fmt.Println("ret1>>>",ret1) // a
}
会上报下面的错误:
cannot convert a (type string) to type int64
2.5 常量
常量是一个简单值的标识符,在程序运行时,不会被修改。常量中的数据类型只可以是布尔型、数字型(整型、浮点型和复数型)和字符串。常量的定义格式如下:
const A string = "wanghw"
const B = "whw"
const C, D = "www", "waa"
常量定义后未被使用,不会在编译时报错。
常量用于枚举
Go语言现阶段没有提供枚举,可以使用常量组模拟枚举。
假设数字0、1和2分别代表未知性别、女性和男性。格式如例:
package main
import "fmt"
const (
Unknown = 0
Female = 1
Male = 2
)
func main(){
fmt.Println("ret>>",Unknown, Female, Male)
// 结果
// ret>> 0 1 2
}
常量组中如果不指定类型和初始值,则与上一行非空常量的值相同:
package main
import "fmt"
const (
a = 10
b
c
)
func main(){
fmt.Println("ret>>",a, b, c)
// 结果
// ret>> 10 10 10
}
iota
iota,特殊常量值,是一个系统定义的可以被编译器修改的常量值。
iota只能被用在常量的赋值中,在每一个const关键字出现时,被重置为0,然后每出现一个常量,iota所代表的数值会自动增加1。
iota可以理解成常量组中常量的计数器,不论该常量的值是什么,只要有一个常量,那么iota就加1。
package main
import "fmt"
const (
a = 12
b = iota
c = iota
)
func main(){
fmt.Println("ret>>",a, b, c)
// 结果
// ret>> 12 1 2
}
常量组中如果不指定类型和初始值,则与上一行非空常量的值相同:
package main
import "fmt"
const (
a = iota
b
c
)
func main(){
fmt.Println("ret>>",a, b, c)
// 结果
// ret>> 0 1 2
}
2.6 类型别名与类型定义
类型别名是Go1.9版本添加的新功能。说到类型别名,无非是给类型名取一个有特殊含义的外号而已,就像武侠小说中的东邪西毒。假如在教室中,有两个同学叫张三,老师为了区分他们,通常会给他们起个别名:大张三、小张三。对于编程而言,类型别名主要用于解决兼容性的问题。
![image-20201022201545626](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022201545626.png)
该语句是将NewString定义为string类型。通过type关键字,NewString会形成一种新的类型。NewString本身依然具备string的特性。
type StringAliaa = string
该语句是将StringAlias定义为string的一个别名。使用StringAlias与string等效。别名类型只会在代码中存在,编译完成时,不会有别名类型。
出于对程序性能的考虑,建议如下:
- 尽可能地使用 := 去初始化声明一个变量(在函数内部)。
- 尽可能地使用字符代替字符串。
2.7 Go语言运算符
运算符用于在程序运行时执行数学或逻辑运算。
Go语言内置的运算符包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、其他运算符。
逻辑运算符
Go语言的逻辑运算符如表所示。假定A值为True,B值为False。
![image-20201022201853056](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022201853056.png)
位运算符
位运算符对整数在内存中的二进制位进行操作。
位运算符比一般的算术运算符速度要快,而且可以实现一些算术运算符不能实现的功能。如果要开发高效率程序,位运算符是必不可少的。位运算符用来对二进制位进行操作,包括:按位与(&)、按位或(|)、按位异或(^)、按位左移(<<)、按位右移(>>)。
Go语言支持的位运算符如表2.15所示。假定A为60,B为13:
![image-20201022202344812](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022202344812.png)
(其他略)
其他运算符
![image-20201022202518777](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022202518777.png)
运算符优先级
![image-20201022202636361](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022202636361.png)
当然,读者可以通过使用括号来临时提升某个表达式的整体运算优先级。
3 Go语言的流程控制
3.1 流程控制概述
3.2 if条件判断语句
![image-20201022203808845](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022203808845.png)
先判断if的布尔表达式,如果为true,其后紧跟的语句块执行,如果为false,再判断else if的布尔表达式,如果为true,其后紧跟的语句块执行,如果为false,再判断下一个else if的布尔表达式,以此类推,当最后一个else if的表达式为false时,执行else语句块。
在if语句的使用过程中,应注意以下细节:
- 不需使用括号将条件包含起来。
- 大括号{}必须存在,即使只有一行语句。
- 左括号必须在if或else的同一行。
- 在if之后,条件语句之前,可以添加变量初始化语句,使用“;”进行分隔。
![image-20201022203944476](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022203944476.png)
if的特殊写法
if语句还有一个变体。它的语法如下所示。
if statement; condition {
// 代码块
}
package main
import "fmt"
func main(){
if num := 10; num %2 == 0{
fmt.Println("偶数")
} else{
fmt.Println("奇数")
}
}
// 结果
// 偶数
需要注意的是,num的定义在if里,那么只能够在该if...else语句块中使用,否则编译器会报错。
3.3 if嵌套语句
![image-20201022204550341](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022204550341.png)
![image-20201022204716080](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022204716080.png)
3.4 switch语句
![image-20201022204847524](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022204847524.png)
switch语句执行的过程自上而下,直到找到case匹配项,匹配项中无须使用break,因为Go语言中的switch默认给每个case自带break。因此匹配成功后不会向下执行其他的 case 分支,而是跳出整个 switch。可以添加fallthrough(中文含义是:贯穿),强制执行后面的case分支。fallthrough必须放在case分支的最后一行。如果它出现在中间的某个地方,编译器就会报错。变量var1可以是任何类型,而val1和val2则可以是同类型的任意值。类型不局限于常量或整数,但必须是相同类型或最终结果为相同类型的表达式。case后的值不能重复,但可以同时测试多个符合条件的值,也就是说case后可以有多个值,这些值之间使用逗号分隔,例如:case val1, val2, val3。switch后的表达式可以省略,默认是switch true。
示例
![image-20201022205034536](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022205034536.png)
接下来再看一个案例,判断某年某月的天数
![image-20201022205101341](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022205101341.png)
类型转换
switch语句还可以被用于type switch(类型转换)来判断某个interface变量中实际存储的变量类型。关于interface变量的知识将在后续的章节中介绍。下面演示type switch的语法。其语法结构如下所示。
![image-20201022205240516](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022205240516.png)
![image-20201022205257559](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022205257559.png)
3.5 for循环语句
循环语句表示当条件满足时,可以反复地执行某段代码。
for是Go语言中唯一的循环语句,Go没有while、do...while循环。
按语法结构来分,Go语言的for循环有4种形式,只有第一种使用分号。
for循环中for关键字后不能加小括号。
语法结构一
for 初始预计init; 条件表达式condition; 结束语句post {
// 循环体代码
}
先执行初始语句,对控制变量赋初始值。初始语句只执行一次。
其次根据控制变量判断条件表达式的返回值,若其值为true,满足循环条件,则执行循环体内语句,之后执行结束语句,开始下一次循环。
执行结束语句之后,将重新计算条件表达式的返回值,如果是true,循环将继续执行,否则循环终止。然后执行循环体外语句。
package main
import "fmt"
func main(){
for i:=0; i<10; i++{
fmt.Printf("%d ", i)
}
}
//0 1 2 3 4 5 6 7 8 9
初始语句、条件表达式和结束语句3种组成部分都是可选的。因此这种基本的for循环语法结构又能演化出4种略有不同的写法。
初始语句是在第一次循环前执行的语句,一般为赋值表达式,给控制变量赋初始值。如果控制变量在此处被声明,其作用域将被局限在这个for的范围内——在for循环中声明的变量仅在循环范围内可用。初始语句可以省略不写,但是初始语句之后的分号必须要写。
省略初始语句的写法:
package main
import "fmt"
func main(){
a := 3
for ; a<5; a++{
fmt.Printf("%d ", a)
}
}
//3 4
条件表达式(condition)是控制循环与否的开关:如果表达式为true,则循环继续;否则结束循环。条件表达式可以省略不写,之后的分号必须要写。省略条件表达式默认形成无限循环。
省略条件表达式的写法:
package main
import "fmt"
func main(){
a := 3
for ; ; a++ {
if a > 5 {
fmt.Printf("%d ", a)
break
}
}
}
//6
结束语句(post),一般为赋值表达式,使控制变量递增或者递减。post语句将在循环的每次成功迭代之后执行。
语法结构二
for关键字后只有1个条件表达式,效果类似其他编程语言中的while循环。其语法结构如下所示。
for 循环条件condition{
// 循环体代码
}
package main
import "fmt"
func main(){
var a int
for a<10{
fmt.Print(a)
a ++
}
}
//0123456789
语法结构三
for关键字后无表达式,效果与其他编程语言的for(;😉 {}一致,此时for执行无限循环。其语法结构如下所示。
for {
// 循环体代码
}
package main
import "fmt"
func main() {
var a int
for {
if a > 5 {
break
}
fmt.Print(a)
a++
}
}
//012345
语法形式四(for ... range)
for循环的range格式对string、slice、array、map、channel等进行迭代循环。
array、slice、string返回索引和值;map返回键和值;channel只返回通道内的值。
其语法结构如下所示:
for key, value := range oldMap{
newMap[key] = value
}
package main
import "fmt"
func main() {
str := "123abC火影王"
for i, value := range str{
fmt.Printf("第 %d 位的ASCII值=%d,字符是%c \n",i, value, value)
}
}
/*
第 0 位的ASCII值=49,字符是1
第 1 位的ASCII值=50,字符是2
第 2 位的ASCII值=51,字符是3
第 3 位的ASCII值=97,字符是a
第 4 位的ASCII值=98,字符是b
第 5 位的ASCII值=67,字符是C
第 6 位的ASCII值=28779,字符是火
第 9 位的ASCII值=24433,字符是影
第 12 位的ASCII值=29579,字符是王
*/
for循环使用案例
求1~100的和
package main
import "fmt"
func main() {
sum := 0
for i:=1; i<=100; i++{
sum += i
}
fmt.Print("sum>>>",sum)
}
//5050
求1~30所有3的倍数的数字的和
package main
import "fmt"
func main() {
i := 1
sum := 0
//死循环
for {
if i>30{
break
}
if i%3 == 0{
sum += i
fmt.Printf("%d",i)
if i<30{
fmt.Print("+")
}else{
fmt.Printf(" = %d \n",sum)
}
}
i++
}
}
//3+6+9+12+15+18+21+24+27+30 = 165
截竹竿问题
截竹竿。32米竹竿,每次截1.5米,至少截几次之后剩余竹竿不足4米?
package main
import "fmt"
func main() {
var length float32
length = 32
i := 0
for length > 4 {
length -= 1.5
i += 1
}
fmt.Printf("一共截了 %d 次", i)
}
//一共截了 19 次
3.6 for循环嵌套
语法结构
Go语言允许在循环体内使用循环。其语法结构如下所示:
for [condition | (init;condition;increment) | Range] {
for [condition | (init;condition;increment) | Range] {
statement(s);
}
statement(s);
}
使用案例
打印直角三角形
package main
import "fmt"
func main() {
// 定义行数
lines := 5
for i:=0;i<lines;i++{
for n:=0;n<2*i+1;n++{
fmt.Print("# ")
}
fmt.Print("\n")
}
}
/*
#
# # #
# # # # #
# # # # # # #
# # # # # # # # #
*/
打印99乘法表
package main
import "fmt"
func main() {
for i:=1;i<=9;i++{ // i控制行数,是乘法表中的第二个数
for j:=1;j<=i;j++{ //j控制列数,是乘法表中的第一个数
fmt.Printf("%d*%d=%d ",j,i,i*j)
}
fmt.Println()
}
}
/*
1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25
1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64
1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81
*/
使用循环嵌套来输出1~50中的素数
package main
import "fmt"
func main() {
fmt.Println("1-50中的素数:")
// 定义局部变量
var a, b int
for a=2;a<=50;a++{
for b=2;b<=(a/b);b++{
if a%b == 0{
// 如果发现因子,则不是素数
break
}
}
if b>(a/b){
fmt.Printf("%d ",a)
}
}
}
/*
1-50中的素数:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
*/
3.7 循环控制语句
break语句
break,跳出循环体。break语句用于终止当前正在执行的for循环,并开始执行循环之后的语句。
package main
import "fmt"
func main() {
for i:=1;i<100;i++{
if i>5{
break
}
fmt.Printf("%d ",i)
}
//循环体外部的代码
fmt.Println()
fmt.Println("newline after for loop")
}
/*
1 2 3 4 5
newline after for loop
*/
continue语句
Go语言的continue语句有点像break语句。但是continue不是跳出循环,而是跳过当前循环,执行下一次循环语句。
for循环中,执行continue语句会触发for增量语句的执行。换言之,continue语句用于跳过for循环的当前迭代,循环将继续到下一个迭代。
package main
import "fmt"
func main() {
//打印1-10中的偶数
for i:=1;i<=10;i++{
if i%2==1{
continue
}
fmt.Printf("%d ",i)
}
}
/*
2 4 6 8 10
*/
break与continue的区别如下:
- break语句无条件跳出并结束当前的循环,然后执行循环体后的语句。
- continue语句跳过当前的循环,而开始执行下一次循环。
goto语句
Go语言的goto语句可以无条件地转移到程序指定的行。
goto语句通常与条件语句配合使用。可用来实现条件转移、构成循环、跳出循环体等功能。
但是,在结构化程序设计中一般不建议使用goto语句,以免造成程序流程的混乱,使理解和调试程序都产生困难。
package main
import "fmt"
func main() {
var C, c int
// 这里不写入for循环是因为for语句执行之初会将C的值变为1,
// 当goto A时,for语句会重新执行(不是重新一轮循环)
C = 1
LOOP:
for C < 50{
C++ // C=1 不能写入for这里就不能写入
for c=2;c<C;c++{
if C%c == 0{
goto LOOP // 若发现因子则不是素数
}
}
fmt.Printf("%d ",C)
}
}
/*
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
*/
4 Go语言的函数与指针
4.1 函数 ***
Go 语言从设计上对函数进行了优化和改进,让函数使用起来更加方便。
因为Go语言的函数本身可以作为值进行传递,既支持匿名函数和闭包,又能满足接口,所以 Go 语言的函数属于一等公民。
函数声明
普通函数需要先声明才能调用,一个函数的声明包括参数和函数名等。编译器通过声明才能了解函数应该怎样在调用代码和函数体之间传入参数和返回参数。语法格式如下所示:
func 函数名(参数列表)(返回参数列表){
// 函数体
}
func funcName(param1 type1, param2 type2,...)(output1 type1, output2 type2,...){
// 逻辑代码
// 返回多个值
return value1, value2,...
}
1. 函数定义解析
func:函数关键字。函数由func开始声明。
funcName:函数名。函数名和参数列表一起构成了函数签名。函数名由字母、数字和下画线组成。函数名的第一个字母不能为数字。在同一个包内,函数不能重名。
param type:参数列表。定义函数时的参数叫作形式参数,形参变量是函数的局部变量;函数被调用时,可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序及参数个数。参数是可选的,也就是说函数可以不包含参数。
output1 type1, output2 type2:返回值列表。返回值返回函数的结果,结束函数的执行。
- Go语言的函数可以返回多个值。
- 返回值可以是返回数据的数据类型,也可以是变量名+变量类型的组合。
- 函数声明时有返回值,必须在函数体中使用return语句提供返回值列表。
- 如果只有一个返回值并且没有声明返回值变量,那么可以省略包括返回值的括号。
- return后的数据,要保持和声明的返回值类型、数量、顺序一致。
- 如果函数没有声明返回值,函数中也可以使用return关键字,用于强制结束函数。
函数体:函数定义的代码集合,是能够被重复调用的代码片段。
2. 参数类型简写
在参数列表中,如果有多个参数变量,则以逗号分隔;如果相邻变量是同类型,则可以将类型省略。语法格式如下所示:
func add (a,b int){}
go语言的函数支持可变参数。接受变参的函数有着不定数量的参数,语法结构如下:
func myfunc(arg ...int){}
arg ...int告诉Go这个函数接受不定数量的参数。
注意,这些参数的类型全部是int。在函数体中,变量arg是一个int的slice(切片)。
变量作用域
作用域是变量、常量、类型、函数的作用范围。
在函数体内声明的变量称为局部变量,它们的作用域只在函数体内,生命周期同所在的函数。参数和返回值变量也是局部变量。
在函数体外声明的变量称为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。全局变量的生命周期同main()。
全局变量可以在任何函数中使用。Go语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。
函数中定义的参数称为形式参数,形式参数会作为函数的局部变量来使用。
为了让大家更直观地理解作用域,下面通过一个案例加以分析:
package main
import "fmt"
// 声明全局变量
var a1 int = 7
var b1 int = 9
// 两数相加的函数
func sum(a1, b1 int)(c1 int){
a1 ++
b1 += 2
c1 = a1 + b1
fmt.Printf("sum函数中的 a1=%d\n", a1) //11
fmt.Printf("sum函数中的 b1=%d\n", b1) //22
fmt.Printf("sum函数中的 c1=%d\n", c1) //33
return c1
}
func main() {
// main 函数中声明局部变量
a1, b1, c1 := 10, 20, 0
// 局部作用域优先用局部作用域的变量
fmt.Printf("main函数中 a1=%d\n", a1) // 10
fmt.Printf("main函数中 b1=%d\n", b1) // 20
fmt.Printf("main函数中 c1=%d\n", c1) // 0
c1 = sum(a1,b1)
fmt.Printf("main函数中的 c1变成了:%d",c1) //33
}
运行结果可以看出,同样变量名的局部变量和全局变量,同一作用域内优先使用局部变量,正所谓“强龙不压地头蛇”。
函数变量(函数作为值)***
在Go语言中,函数也是一种类型,可以和其他类型(如int32、float等等)一样被保存在变量中。
在Go语言中可以通过type来定义一个自定义类型。函数的参数完全相同(包括参数类型、个数、顺序),函数返回值相同。
函数变量案例一
package main
import (
"fmt"
"strings"
)
func main() {
result := StringToLower("ADSDFDSKJLkjklsdUOSDFSDF",processCase) // 7行
fmt.Println(result) //AdSdFdSkJlKjKlSdUoSdFsDf
result = StringToLower2("ADSDFDSKJLkjklsdUOSDFSDF",processCase)
fmt.Println(result) //AdSdFdSkJlKjKlSdUoSdFsDf
}
// 处理字符串,奇数偶数依次显示为大小写
func processCase(str string) string{ // 13行
ret := ""
for i,value := range str{
if i%2 ==0{
ret += strings.ToUpper(string(value))
}else{
ret += strings.ToLower(string(value))
}
}
return ret
}
func StringToLower(str string, f func(string) string) string{ // 24行
fmt.Printf("StringToLower>>> %T \n",f) //func(string) string
return f(str)
}
type caseFunc func(string) string // 28行:声明了一个函数类型,通过type关键字,caseFunc会形成一种新的类型
func StringToLower2(str string, f caseFunc) string{
// 打印变量f的类型
fmt.Printf("StringToLower2>>> %T \n",f) //main.caseFunc
return f(str)
}
在上例中,第24行声明了函数StringToLower(),第二个传入参数为一个函数类型变量f。第7行调用了函数StringToLower(),第二个参数传入了在第13行声明的函数processCase()。这样在函数StringToLower()中可以通过函数变量f执行函数processCase()。第28行将参数相同的函数类型声明为新的类型caseFunc,那么caseFunc就代表着这一种函数变量使用在StringToLower2()中。
函数变量案例二
package main
import "fmt"
type processFunc func(int) bool // 声明了一个函数类型
func main() {
slice := []int{1, 2, 3, 4, 5, 7}
fmt.Println("slice = ",slice)
odd := filter(slice, isOdd) // 函数当作值来传递
fmt.Println("奇数元素:", odd)
even := filter(slice, isEven) // 函数当作值传递
fmt.Println("偶数元素:", even)
}
//判断元素是否是偶数
func isEven(integer int) bool{
if integer%2 == 0{
return true
}
return false
}
//判断元素是否是奇数
func isOdd(integer int) bool{
if integer%2 == 0{
return false
}
return true
}
//根据函数来处理切片,根据元素的奇数偶数分组,返回新的切片
func filter(slice []int, f processFunc) []int{
var ret []int
for _, value := range(slice){
if f(value){
ret = append(ret,value)
}
}
return ret
}
/*
slice = [1 2 3 4 5 7]
奇数元素: [1 3 5 7]
偶数元素: [2 4]
*/
函数变量的使用步骤及意义如下:
- 定义一个函数类型。
- 实现定义的函数类型。
- 作为参数调用。
函数变量的用法类似接口的用法。
函数当作值和类型在写一些通用接口的时候非常有用,通过上面的例子可以看到processFunc这个类型是一个函数类型,然后两个filter函数的参数和返回值与processFunc类型是一样的。用户可以实现很多种逻辑,这样使得程序变得非常灵活。
匿名函数
Go语言支持匿名函数,即在需要使用函数时再定义函数。匿名函数没有函数名,只有函数体,函数可以作为一种类型被赋值给变量,匿名函数也往往以变量方式被传递。
匿名函数经常被用于实现回调函数、闭包等。语法格式如下所示:
func(参数列表)(返回参数列表){
// 函数体
}
1. 咱定义时调用匿名函数
package main
import "fmt"
func main(){
func(data int){
fmt.Println("hello!",data)
}(250)
}
//hello! 250
2. 将匿名函数赋值给变量
package main
import "fmt"
func main(){
f := func(data string){
fmt.Println(data)
}
f("火影whw")
}
//火影whw
3. 匿名函数用作回调函数 ***
package main
import (
"fmt"
"math"
)
func main(){
// 调用函数,对每个元素进行求平方根的操作
arr := []float64{1,9,16,25,33}
visit(arr, func(v float64){
v = math.Sqrt(v)
fmt.Printf("平方根:%.2f ",v)
})
println()
// 调用函数,对每个元素进行求平方操作
visit(arr, func(v float64){
v = math.Pow(v,2)
fmt.Printf("平方:%.0f ", v)
})
}
// 定义一个函数,遍历切片元素,对每个元素进行处理
func visit(lst []float64, f func(float64)){
for _, value := range lst{
f(value)
}
}
/*
平方根:1.00 平方根:3.00 平方根:4.00 平方根:5.00 平方根:5.74
平方:1 平方:81 平方:256 平方:625 平方:1089
*/
闭包closure ***
1. 闭包的概念
闭包并不是什么新奇的概念,它早在高级语言开始发展的年代就产生了。闭包(Closure)是词法闭包(Lexical Closure)的简称。
闭包是由函数和与其相关的引用环境组合而成的实体。在实现深约束时,需要创建一个能显式表示引用环境的东西,并将它与相关的子程序捆绑在一起,这样捆绑起来的整体被称为闭包。函数 + 引用环境 = 闭包。
闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定了,不会在执行时发生变化,所以一个函数只有一个实例。
闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
闭包在某些编程语言中被称为Lambda表达式。
函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”。函数是编译器静态的概念,而闭包是运行期动态的概念。
对象是附有行为的数据,而闭包是附有数据的行为。
2. 闭包的优点
(1)加强模块化。闭包有益于模块化编程,便于以简单的方式开发较小的模块,从而提高开发速度和程序的可复用性。和没有使用闭包的程序相比,使用闭包可将模块划分得更小。
比如要计算一个数组中所有数字的和,只需要循环遍历数组,把遍历到的数字加起来就行了。如果现在要计算所有元素的积,又或者要打印所有的元素呢?解决这些问题都要对数组进行遍历,如果是在不支持闭包的语言中,程序员不得不一次又一次重复地写循环语句。而这在支持闭包的语言中是不必要的。这种处理方法多少有点像回调函数,不过要比回调函数写法更简单,功能更强大。
(2)抽象。闭包是数据和行为的组合,这使得闭包具有较好的抽象能力。
(3)简化代码。一个编程语言需要以下特性来支持闭包:
- 函数是一阶值(First-class value,一等公民),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值。
- 函数可以嵌套定义,即在一个函数内部可以定义另一个函数。
- 允许定义匿名函数。
- 可以捕获引用环境,并把引用环境和函数代码组成一个可调用的实体。
由于闭包函数“捕获”了和它在同一作用域的其他常量和变量,所以当闭包在任何地方被调用,闭包都可以使用这些常量或者变量。它不关心这些变量是否已经超出作用域,只要闭包还在使用这些变量,这些变量就依然存在。
没有使用闭包进行计数的代码:
package main
import "fmt"
func main(){
for i:=0;i<5;i++{
fmt.Printf("i=%d ",i)
fmt.Println(add2(i))
}
}
func add2(x int) int{
sum := 0
sum += x
return x
}
// for循环每执行一次,sum都会清零,没有实现sum累加计数。
/*
i=0 0
i=1 1
i=2 2
i=3 3
i=4 4
*/
使用闭包函数实现计数器:
package main
import "fmt"
func main(){
pos := adder()
for i := 0;i<=5;i++{
fmt.Printf("i=%d ",i)
fmt.Println(pos(i))
}
fmt.Println("------------------继续在之前的基础上累和-----------------")
for i := 0;i<=5;i++{
fmt.Printf("i=%d ",i)
fmt.Println(pos(i))
}
}
func adder() func(int) int{
sum := 0
return func(x int) int{
fmt.Printf("sum1=%d ",sum)
sum += x
fmt.Printf("sum2=%d ",sum)
return sum
}
}
/*
i=0 sum1=0 sum2=0 0
i=1 sum1=0 sum2=1 1
i=2 sum1=1 sum2=3 3
i=3 sum1=3 sum2=6 6
i=4 sum1=6 sum2=10 10
i=5 sum1=10 sum2=15 15
------------------继续在之前的基础上累和-----------------
i=0 sum1=15 sum2=15 15
i=1 sum1=15 sum2=16 16
i=2 sum1=16 sum2=18 18
i=3 sum1=18 sum2=21 21
i=4 sum1=21 sum2=25 25
i=5 sum1=25 sum2=30 30
*/
闭包的案例
package main
import "fmt"
func main(){
myfunc := Counter()
fmt.Println("myfunc",myfunc)//myfunc 0x109f170
// 调用 myfunc函数,i变量自增1并返回
fmt.Println(myfunc())//1
fmt.Println(myfunc())//2
fmt.Println(myfunc())//3
// 创建新的函数 nextNumber1,并查看结果
myfunc1 := Counter()
fmt.Println("myfunc1>>",myfunc1)//0x109f180
fmt.Println("myfunc1的ret>>",myfunc1()) //1
fmt.Println("myfunc1的ret>>",myfunc1()) //2
}
// 计数器,闭包函数
func Counter() func() int{
i := 0
res := func() int{
i += 1
return i
}
fmt.Printf("%T, %v \n", res, res) //func() int, 0x109f170
fmt.Println("Counter中的内部函数:",res) //Counter中的内部函数: 0x109f170
return res
}
可变参数
如果一个函数的参数,类型一致,但个数不定,可以使用函数的可变参数。语法格式如下所示
func 函数名(参数名 ...类型)[返回值列表]{
// 函数体
}
该语法格式定义了一个接受任何数目、任何类型参数的函数。
这里特殊的语法是三个点“...”,在一个变量后面加上三个点,表示从该处开始接受可变参数。
当要传递若干个值到可变参数函数中时,可以手动书写每个参数,也可以将一个slice传递给该函数,通过“...”可以将slice中的参数对应地传递给函数。
package main
import "fmt"
func main(){
sum, avg, count := GetScore(90,87,86,92)
fmt.Printf("sum=%.2f, avg=%.2f, count=%d",sum,avg,count)//sum=355.00, avg=88.75, count=4
fmt.Println()
// 列表传不定参数,后面跟...
scores := []float64{99,88,55,21.1,97,3}
sum2, avg2, count2 := GetScore(scores...)
fmt.Printf("sum=%.2f, avg=%.2f, count=%d",sum2,avg2,count2)//sum=363.10, avg=60.52, count=6
}
func GetScore(scores ...float64)(sum, avg float64,count int){
for _,value := range scores{
sum += value
count ++
}
avg = sum / float64(count)
return
}
使用可变参数应注意如下细节:
- 一个函数最多只能有一个可变参数。
- 若参数列表中还有其他类型参数,则可变参数写在所有参数的最后。
递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身,那么这个函数就是递归函数。递归函数必须满足以下两个条件。
(1)在每一次调用自己时,必须是(在某种意义上)更接近于解。
(2)必须有一个终止处理或计算的准则。
下面通过案例来理解递归函数的作用。
计算阶乘n!=1×2×3×...×n,用函数fact(n)表示,可以看出:fact(n)=n!=1×2×3×...×(n-1)×n=(n-1)!×n=fact(n-1)×n。所以,fact(n)可以表示为n×fact(n-1),只有n=1时需要特殊处理。如例4-11所示。
![image-20201024163241164](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024163241164.png)
递归的计算过程如下所示。
![image-20201024163301394](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024163301394.png)
使用递归需要注意如下事项:
- 递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以用循环的方式实现,但循环的逻辑不如递归清晰。
- 使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层,每当函数返回,栈就会减一层。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
- 使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。
4.2 指针 ***
指针的概念
指针是存储另一个变量的内存地址的变量。变量是一种使用方便的占位符,变量都指向计算机的内存地址。
一个指针变量可以指向任何一个值的内存地址。
例如:变量b的值为156,存储在内存地址0x1040a124。变量a持有b的地址,则a被认为指向b。如图4.12所示。
![image-20201024163544887](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024163544887.png)
在Go语言中使用取地址符(&)来获取变量的地址,一个变量前使用&,会返回该变量的内存地址。
package main
import "fmt"
func main(){
a := 10
fmt.Printf("变量a的地址:%x\n",&a) // 变量a的地址:c00001e060
}
Go语言指针的特点如下:
- Go语言指针的最大特点是:指针不能运算(不同于C语言)。
- 在Go语言中如果对指针进行运算会报错!
声明指针
声明指针,*T是指针变量的类型,它指向T类型的值。
var 指针变量名 *指针类型
*号用于指定变量是一个指针:
var ip *int // 指向整型的指针
var fp *float32 // 指向浮点型的指针
指针使用流程如下:
- 定义指针变量。
- 为指针变量赋值。
- 访问指针变量中指向地址的值。
获取指针指向的变量值:在指针类型的变量前加上 * 号(前缀),如*a。
指针示例一
package main
import "fmt"
func main(){
// 声明实际变量
var a int = 120
// 声明指针变量
var ip *int
// 给指针变量赋值,将变量a的地址赋值给ip
ip = &a
// 打印 a 的类型和值
fmt.Printf("a的类型:%T,值是:%v \n",a,a)//a的类型:int,值是:120
// 打印 &a 的类型与值
fmt.Printf("&a的类型:%T,值是:%v \n",&a,&a)//&a的类型:*int,值是:0xc0000b4008
// 打印 ip 的类型和值
fmt.Printf("ip的类型:%T,值是:%v \n",ip,ip)//ip的类型:*int,值是:0xc0000b4008
// 打印 *ip 的类型和值
fmt.Printf("*ip的类型:%T,值是:%v \n",*ip,*ip)//*ip的类型:int,值是:120
// 打印 *&a 的类型和值
fmt.Printf("*&a的类型:%T,值是:%v \n",*&a,*&a)//*&a的类型:int,值是:120
}
指针示例二
![image-20201024165219705](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024165219705.png)
结果如下:
![image-20201024165236414](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024165236414.png)
空指针
在Go语言中,当一个指针被定义后没有分配到任何变量时,它的值为nil。nil指针也称为空指针。nil在概念上和其他语言的null、None、NULL一样,都指代零值或空值。
假设指针变量命名为ptr。空指针判断如下:
if ptr != nil // ptr不是空指针
if ptr == nil // ptr是空指针
使用指针***
1. 通过指针修改变量的数值
package main
import "fmt"
func main(){
b := 996
a := &b
fmt.Println("b的地址:",a)
fmt.Println("*a的值:",*a)
*a++
fmt.Println("b的新值:",b)
}
/*
b的地址: 0xc0000b4008
*a的值: 996
b的新值: 997
*/
2. 使用指针作为函数的参数
package main
import "fmt"
func main(){
a := 58
fmt.Printf("%T \n",a) //int
fmt.Printf("%x \n",&a)//c00012c008
// b := &a
var b *int = &a
change(b)
fmt.Println("函数调用之后的a的值:",a) //函数调用之后的a的值: 15
}
func change(val *int){
*val = 15
}
将基本数据类型的指针作为函数的参数,可以实现对传入数据的修改,这是因为指针作为函数的参数只是复制了一个指针,指针指向的内存没有发生改变。
指针数组
指针数组:就是元素为指针类型的数组。
var ptr [3]*string
有一个元素个数与之相同的数组,将该数组中每个元素的地址赋值给该指针数组。也就是说该指针数组与某一个数组完全对应。可以通过*指针变量获取到该地址所对应的数值。
package main
import "fmt"
const COUNT int = 3
func main() {
a := [COUNT]string{"abc", "ABC", "whw"}
// 定义指针数组
var ptr [COUNT]*string
fmt.Printf("%T, %v \n", ptr, ptr) //[3]*string, [<nil> <nil> <nil>]
for i := 0; i < COUNT; i++ {
// 将数组中每个元素的地址赋值给指针数组
ptr[i] = &a[i]
}
fmt.Printf("%T, %v \n", ptr, ptr) //[3]*string, [0xc000090180 0xc000090190 0xc0000901a0]
// 获取指针数组中的第一个值,其实就是一个地址
fmt.Println(ptr[0]) //0xc000090180
// 根据数组元素的每个地址获取该地址所指向的元素的数值
for i := 0; i < COUNT; i++ {
fmt.Printf("a[%d]=%s \n", i, *ptr[i])
}
/*
a[0]=abc
a[1]=ABC
a[2]=whw
*/
}
指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址,如图4.19所示。
![image-20201024171840961](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024171840961.png)
指向指针的指针变量声明格式如下。
var ptr **int
以上指向指针的指针变量为整型。
访问指向指针的指针变量值需要使用两个 * 号。
package main
import "fmt"
func main(){
var a string
var ptr *string
var pptr **string
a = "火影whw"
// 指针ptr地址
ptr = &a
fmt.Println("ptr>>>",ptr)//0xc0000901e0
// pptr:指向指针ptr的指针
pptr = &ptr
fmt.Println("pptr>>>",pptr)//0xc0000b0018
// 获取具体的值
fmt.Printf("a=%v \n",a)
fmt.Printf("*ptr=%v \n",*ptr)
fmt.Printf("**pptr=%v \n",**pptr)
/*
a=火影whw
*ptr=火影whw
**pptr=火影whw
*/
}
4.3 函数的参数传递
函数如果使用参数,该参数变量称为函数的形参。形参就像定义在函数体内的局部变量。
调用函数,可以通过两种方式来传递参数,即值传递和引用传递,或者叫作传值和传引用。
值传递(传值)
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到原内容数据。
默认情况下,Go语言使用的是值传递,即在调用过程中不会影响到原内容数据。
每次调用函数,都将实参复制一份再传递到函数中。每次都复制一份,性能会下降,但是Go 语言中使用指针和值传递配合就避免了性能降低问题,也就是通过传指针参数来解决实参复制的问题。
引用传递(传引用)
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到原内容数据。
严格来说Go语言只有值传递这一种传参方式,Go语言是没有引用传递的。
Go语言中可以借助传指针来实现引用传递的效果。函数参数使用指针参数,传参的时候其实是复制一份指针参数,也就是复制了一份变量地址。
函数的参数如果是指针,当函数调用时,虽然参数仍然是按复制传递的,但是此时仅仅只是复制一个指针,也就是一个内存地址,这样就不用担心实参复制造成的内存浪费、时间开销、性能降低。
引用传递的作用如下:
- 传指针使得多个函数能操作同一个对象。
- 传指针更轻量级(8 bytes),只需要传内存地址。如果参数是非指针参数,那么值传递的过程中,每次在复制上面就会花费相对较多的系统开销(内存和时间)。所以要传递大的结构体的时候,用指针是一个明智的选择。
Go语言中slice、map、chan类型的实现机制都类似指针,所以可以直接传递,而不必取地址后传递指针。
函数传int类型的值与引用的对比:
package main
import "fmt"
func main(){
a := 10
fmt.Printf("1. 变量a的内存地址:%p,值为%v \n",&a,a)
fmt.Printf("======int型变量a的内存地址:%p \n",a)
changeIntVal(a)
fmt.Printf("2. changeIntVal函数之后,变量a的内存地址:%p,值为:%v \n",&a,a) // a还是10
changeIntPtr(&a)
fmt.Printf("3. changeIntPtr函数之后,变量a的内存地址:%p,值为:%v \n",&a,a) // a变成了50
}
//函数传int类型的值
func changeIntVal(a int){
fmt.Printf("------changeIntVal函数内:值参数a的内存地址:%p,值为:%v\n",&a,a)
// 改变a
a = 90
}
//函数传引用
func changeIntPtr(a *int){
fmt.Printf("------changeIntPtr函数内:指针参数a的内存地址:%p,值为:%v\n",&a,a)
// 改变a
*a = 50
}
/*
1. 变量a的内存地址:0xc0000b4008,值为10
======int型变量a的内存地址:%!p(int=10)
------changeIntVal函数内:值参数a的内存地址:0xc0000b4020,值为:10
2. changeIntVal函数之后,变量a的内存地址:0xc0000b4008,值为:10
------changeIntPtr函数内:指针参数a的内存地址:0xc0000ae020,值为:0xc0000b4008
3. changeIntPtr函数之后,变量a的内存地址:0xc0000b4008,值为:50
*/
函数传slice类型的值与引用的对比
package main
import "fmt"
func main(){
a := []int{1,2,3,4}
fmt.Printf("1. 变量a的内存地址:%p,值为%v \n",&a,a)
fmt.Printf("======int型变量a的内存地址:%p \n",a)
changeIntVal(a)
fmt.Printf("2. changeIntVal函数之后,变量a的内存地址:%p,值为:%v \n",&a,a) // a变成了[90 2 3 4]
changeIntPtr(&a)
fmt.Printf("3. changeIntPtr函数之后,变量a的内存地址:%p,值为:%v \n",&a,a) // a变成了[50 2 3 4]
}
//函数传int类型的值
func changeIntVal(a []int){
fmt.Printf("------changeIntVal函数内:值参数a的内存地址:%p,值为:%v\n",&a,a)
// 改变a
a[0] = 90
}
//函数传引用
func changeIntPtr(a *[]int){
fmt.Printf("------changeIntPtr函数内:指针参数a的内存地址:%p,值为:%v\n",&a,a)
// 改变a
(*a)[0] = 50
}
/*
1. 变量a的内存地址:0xc00000c080,值为[1 2 3 4]
======int型变量a的内存地址:0xc000018100
------changeIntVal函数内:值参数a的内存地址:0xc00000c0e0,值为:[1 2 3 4]
2. changeIntVal函数之后,变量a的内存地址:0xc00000c080,值为:[90 2 3 4]
------changeIntPtr函数内:指针参数a的内存地址:0xc00000e030,值为:&[90 2 3 4]
3. changeIntPtr函数之后,变量a的内存地址:0xc00000c080,值为:[50 2 3 4]
*/
函数传数组,其类型的值与引用的对比
package main
import "fmt"
func main(){
a := [4]int{1,2,3,4}
fmt.Printf("1. 变量a的内存地址:%p,值为%v \n",&a,a)
fmt.Printf("======int型变量a的内存地址:%p \n",a)
changeIntVal(a)
fmt.Printf("2. changeIntVal函数之后,变量a的内存地址:%p,值为:%v \n",&a,a) // a没变 还是[1 2 3 4]
changeIntPtr(&a)
fmt.Printf("3. changeIntPtr函数之后,变量a的内存地址:%p,值为:%v \n",&a,a) // a变成了[50 2 3 4]
}
//函数传int类型的值
func changeIntVal(a [4]int){
fmt.Printf("------changeIntVal函数内:值参数a的内存地址:%p,值为:%v\n",&a,a)
// 改变a
a[0] = 90
}
//函数传引用
func changeIntPtr(a *[4]int){
fmt.Printf("------changeIntPtr函数内:指针参数a的内存地址:%p,值为:%v\n",&a,a)
// 改变a
(*a)[0] = 50
}
/*
1. 变量a的内存地址:0xc0000b8000,值为[1 2 3 4]
======int型变量a的内存地址:%!p([4]int=[1 2 3 4])
------changeIntVal函数内:值参数a的内存地址:0xc0000b8080,值为:[1 2 3 4]
2. changeIntVal函数之后,变量a的内存地址:0xc0000b8000,值为:[1 2 3 4]
------changeIntPtr函数内:指针参数a的内存地址:0xc0000ae020,值为:&[1 2 3 4]
3. changeIntPtr函数之后,变量a的内存地址:0xc0000b8000,值为:[50 2 3 4]
*/
函数传结构体
package main
import "fmt"
type Teacher struct{
name string
age int
married bool
}
func main(){
a := Teacher{"wanghe",22,false}
fmt.Printf("1. 变量a的内存地址:%p,值为%v \n",&a,a)
fmt.Printf("======int型变量a的内存地址:%p \n",a)
changeIntVal(a)
fmt.Printf("2. changeIntVal函数之后,变量a的内存地址:%p,值为:%v \n",&a,a) // a没变 还是{wanghe 22 false}
changeIntPtr(&a)
fmt.Printf("3. changeIntPtr函数之后,变量a的内存地址:%p,值为:%v \n",&a,a) // a变成了{sasuke 32 false}
}
//函数传int类型的值
func changeIntVal(a Teacher){
fmt.Printf("------changeIntVal函数内:值参数a的内存地址:%p,值为:%v\n",&a,a)
// 改变a
a.name = "naruto"
a.age = 23
}
//函数传引用
func changeIntPtr(a *Teacher){
fmt.Printf("------changeIntPtr函数内:指针参数a的内存地址:%p,值为:%v\n",&a,a)
// 改变a
a.name = "sasuke"
a.age = 32
}
/*
1. 变量a的内存地址:0xc00000c080,值为{wanghe 22 false}
======int型变量a的内存地址:%!p(main.Teacher={wanghe 22 false})
------changeIntVal函数内:值参数a的内存地址:0xc00000c0e0,值为:{wanghe 22 false}
2. changeIntVal函数之后,变量a的内存地址:0xc00000c080,值为:{wanghe 22 false}
------changeIntPtr函数内:指针参数a的内存地址:0xc00000e030,值为:&{wanghe 22 false}
3. changeIntPtr函数之后,变量a的内存地址:0xc00000c080,值为:{sasuke 32 false}
*/
值传递与引用传递的细节问题
Go语言中所有的传参都是值传递(传值),都是一个副本。
副本的内容有的是值类型(int、string、bool、array、struct属于值类型),这样在函数中就无法修改原内容数据;
有的是引用类型(pointer、 slice、map、chan属于引用类型),这样就可以修改原内容数据。
是否可以修改原内容数据,和传值、传引用没有必然的关系。在C++中,传引用肯定是可以修改原内容数据的;在Go语言里,虽然只有传值,但是也可以修改原内容数据,因为参数可以是引用类型。
传引用和引用类型是两个概念。虽然Go语言只有传值一种方式,但是可以通过传引用类型变量达到与传引用一样的效果。
5 Go语言的内置容器
5.1 数组
数组的概念
数组是相同类型的一组数据构成的长度固定的序列,其中数据类型包含了基本数据类型、复合数据类型和自定义类型。数组中的每一项被称为数组的元素。数组名是数组的唯一标识符,数组的每一个元素都是没有名字的,只能通过索引下标(位置)进行访问。
因为数组的内存是一段连续的存储区域,所以数组的检索速度是非常快的;
但是数组也有一定的缺陷,就是定义后长度不能更改。
数组的语法
Go语言数组声明需要指定元素类型及元素个数,语法格式如下。
var 变量名 [数组长度] 数据类型
以上为一维数组的定义方式,数组长度必须是整数且大于0,未初始化的数组不是nil,也就是说没有空数组(与切片不同)。
初始化数组语法格式如下。
var nums = [5]{1,2,3,4,5}
初始化数组中 {} 中的元素个数不能大于 [] 中的数字。
如果忽略 [] 中的数字,不设置数组长度,Go语言会根据元素的个数来设置数组的长度。可以忽略声明中数组的长度并将其替换为“…”。编译器会自动计算长度。语法格式如下。
var nums = [...]int{1,2,3,4,5,6}
修改数组内容,语法格式如下:
nums[4] = 23
以上实例读取数组第5个元素。数组元素可以通过索引(位置)来读取(或者修改),索引从0开始,第1个元素索引为0,第2个索引为1,以此类推。
数组的长度
数组的长度是数组的一个内置常量,通过将数组作为参数传递给 len()函数,可以获得数组的长度。忽略声明中数组的长度并将其替换为“…”,编译器可以找到长度。接下来使用案例演示获取数组长度的方式:
package main
import "fmt"
func main(){
a := [4]int64{12,22,33,55}
b := [...]float64{23.3,22,23.123,12.123}
fmt.Printf("len_a:%d,len_b:%d \n",len(a),len(b)) //len_a:4,len_b:4
}
遍历数组
package main
import "fmt"
func main(){
a := [4]int64{12,22,33,55}
//b := [...]float64{23.3,22,23.123,12.123}
// 遍历方式1
for i:= 0;i<len(a);i++{
fmt.Printf("%d ",a[i])
}
fmt.Println()
// 遍历方式2
for _, value := range a{
fmt.Printf("%d ",value)
}
}
多维数组——主要是二维数组
由于数据的复杂程度不一样,数组可能有多个下标。一般将数组元素下标的个数称为维数,根据维数,可将数组分为一维数组、二维数组、三维数组、四维数组等。二维及以上的数组可称为多维数组。
Go语言的多维数组声明方式:
var variable_name [SIZE1][SIZE2]...[SIZEn] variable_type
二维数组
在实际的工作中,仅仅使用一维数组是远远不够的,例如,一个学习小组有10个人,每个人有3门课的考试成绩,如果使用一维数组解决是很麻烦的。这时,可以使用二维数组。
二维数组是最简单的多维数组,二维数组的本质也是一个一维数组,只是数组成员由基本数据类型变成了构造数据类型(一维数组)。
二维数组的定义方式如下。
var arrayName [x][y] variable_type
二维数组初始化,语法格式如下:
a = [3][4]int{
{0,1,2,3}, // 第一行索引为0
{4,5,6,7}, // 第二行索引为1
{8,9,10,11} // 第三行索引为2
}
上述定义的二维数组共包含3×4个元素,即12个元素。接下来,我们通过一张图来描述二维数组a的元素分布情况,如图5.3所示。
![image-20201024182415812](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024182415812.png)
二维数组元素通过指定坐标来访问,如数组中的行索引与列索引。语法格式如下。
int val = a[2][3]
以上实例访问了二维数组val第3行的第4个元素。
二维数组可以使用循环嵌套来输出元素,具体语法通过案例演示
![image-20201024182517611](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024182517611.png)
结果如下:
![image-20201024182529965](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024182529965.png)
数组是值类型
Go语言中的数组并非引用类型,而是值类型。
当它们被分配给一个新变量时,会将原始数组复制出一份分配给新变量。
因此对新变量进行更改,原始数组不会有反应。
package main
import "fmt"
func main(){
a := [...]string{"USA","China","India","Germany"}
b := a // 将a赋值给b
b[0] = "Singapore"
fmt.Println("a>>>",a) // [USA China India Germany]
fmt.Println("b>>>",b) // [Singapore China India Germany]
}
注意:将数组作为函数参数进行传递,它们将通过值传递,原始数组依然保持不变。
5.2 切片***
切片的概念
Go语言中数组的长度不可改变,但在很多应用场景中,在初始定义数组时,数组的长度并不可预知,这样的序列集合无法满足要求。Go中提供了另外一种内置类型“切片(slice)”,弥补了数组的缺陷。切片是可变长度的序列,序列中每个元素都是相同的类型。切片的语法和数组很像。
从底层来看,切片引用了数组的对象。切片可以追加元素,在追加时可能使切片的容量增大。与数组相比,切片不需要设定长度,在[]中不用设定值,相对来说比较自由。
切片的数据结构可理解为一个结构体,这个结构体包含了三个元素。
- 指针,指向数组中切片指定的开始位置。
- 长度,即切片的长度。
- 容量,也就是切片开始位置到数组的最后位置的长度。
切片的语法
1. 声明切片
声明一个未指定长度的数组来定义切片,具体示例如下:
var indentifier []type
切片不需要说明长度。采用该声明方式且未初始化的切片为空切片。该切片默认为nil,长度为0。
使用make()函数来创建切片,语法格式如下:
var slice1 []type make([]type,len)
使用make()函数来创建切片可以简写为如下格式:
slice1 := make([]type,len)
创建切片时可以指定容量,其中capacity为可选参数:make([]T, length,capacity)。详情如下:
package main
import "fmt"
func main(){
var nums = make([]int,3,5)
fmt.Printf("%T\n",nums)//[]int
fmt.Printf("len=%d cap=%d slice=%v\n",len(nums),cap(nums),nums)
//len=3 cap=5 slice=[0 0 0]
}
2. 初始化
(1)直接初始化切片,语法格式如下:
s := [] int {1,2,3}
(2)通过数组截取来初始化切片,语法格式如下:
arr := [5]int {1,2,3,4,5}
s := arr[:]
切片中包含数组所有元素,语法格式如下。
s := arr[startIndex : endIndex]
将arr中从下标startIndex到endIndex-1下的元素创建为一个新的切片(前闭后开),长度为endIndex-startIndex。
缺省endIndex时表示一直到arr的最后一个元素,语法格式如下。
s := arr[startIndex:]
缺省startIndex时表示从arr的第一个元素开始,语法格式如下。
s := arr[:endIndex]
(3)通过切片截取来初始化切片。
可以通过设置下限及上限来设置截取切片:[lower-bound:upper-bound]:
package main
import "fmt"
func main(){
// 创建切片
nums := []int{0,1,2,3,4,5,6,7,8,9}
printSlice(nums) //len=10,cap=10,slice=[0 1 2 3 4 5 6 7 8 9]
// 从索引1(包含)到4(不包含)
fmt.Println("1->4:::",nums[1:4]) //1->4::: [1 2 3]
// 使用切片初始化
nums1 := nums[1:4]
printSlice(nums1)//len=3,cap=9,slice=[1 2 3]
}
func printSlice(x []int){
fmt.Printf("len=%d,cap=%d,slice=%v\n",len(x),cap(x),x)
}
Len()和cap()函数
切片的长度是切片中元素的数量。切片的容量是从创建切片的索引开始的底层数组中元素的数量。
切片可以通过len()方法获取长度,可以通过cap()方法获取容量。数组计算cap()结果与len()相同。
切片是引用类型 ***
切片没有自己的任何数据。它只是底层数组的一个引用。对切片所做的任何修改都将反映在底层数组中。数组是值类型,而切片是引用类型,两者的区别如下例所示:
package main
import "fmt"
func main(){
a := [4]float64{56.1,22.2,33.12,55.45}
b := []int{2,3,5}
fmt.Printf("变量a —— 地址:%p,类型:%T,数值:%v,长度:%d\n",&a,a,a,len(a))
fmt.Printf("变量b —— 地址:%p,类型:%T,数值:%v,长度:%d\n",&b,b,b,len(b))
c := a
d := b
fmt.Printf("变量c —— 地址:%p,类型:%T,数值:%v,长度:%d\n",&c,c,c,len(c))
fmt.Printf("变量d —— 地址:%p,类型:%T,数值:%v,长度:%d\n",&d,d,d,len(d))
c[1] = 666
fmt.Println("a=",a,"c=",c) // a= [56.1 22.2 33.12 55.45] c= [56.1 666 33.12 55.45]
// 因为切片是引用类型(跟Python一样!)所以改变d的话同时会影响b中的数据!!!
d[1] = 999
fmt.Println("b=",b,"d=",d) // b= [2 999 5] d= [2 999 5]
}
append()和copy()函数
两个方法不适用于数组。
函数append()用于往切片中追加新元素,可以向切片里面追加一个或者多个元素,也可以追加一个切片。append()会改变切片所引用的数组的内容,从而影响到引用同一数组的其他切片。当使用append()追加元素到切片时,如果容量不够(也就是(cap-len) == 0),Go就会创建一个新的内存地址来储存元素。
函数copy()会复制切片元素,将源切片中的元素复制到目标切片中,返回复制的元素的个数。 copy()方法不会建立源切片与目标切片之间的联系。也就是两个切片不存在联系,其中一个修改不影响另一个。
删除第一个或最后一个元素的方法:
![image-20201024190435588](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024190435588.png)
删除中间元素的方法:
![image-20201024190457761](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024190457761.png)
append()与copy()的对比案例一***
package main
import "fmt"
// 输出切片格式化信息
func printSlices(name string,x []int){
fmt.Print(name,"\t")
fmt.Printf("addr:%p len=%d cap=%d slice=%v \n",x,len(x),cap(x),x)
}
func main(){
fmt.Println("1. ----------------------------")
numbers := make([]int, 0, 20)
printSlices("numbers",numbers) //numbers addr:0xc00006e000 len=0 cap=20 slice=[]
//append
numbers = append(numbers,0,1)
printSlices("numbers",numbers) //numbers addr:0xc00006e000 len=2 cap=20 slice=[0 1]
fmt.Println("2. ----------------------------")
//追加一个切片
s1 := []int{100,200,300,400,500}
numbers = append(numbers,s1...)
printSlices("numbers",numbers) // numbers addr:0xc000134000 len=7 cap=20 slice=[0 1 100 200 300 400 500]
fmt.Println("3. ----------------------------")
// 切片删除元素
// 删除第一个元素
numbers = numbers[1:]
// 删除最后一个元素
numbers = numbers[:len(numbers)-1]
// 删除中间元素
a := int(len(numbers)/2) // 中间数
numbers = append(numbers[:a],numbers[a+1:]...)
fmt.Println("4. ----------------------------")
// 创建切片 mumbers1 是之前切片的两倍容量
numbers1 := make([]int,len(numbers),(cap(numbers))*2)
// 复制numbers的内容到numbers1 —— count是复制的个数
count := copy(numbers1,numbers)
fmt.Println("复制的个数:",count) // 4
printSlices("numbers1",numbers1)//numbers1 addr:0xc000070000 len=4 cap=38 slice=[1 100 300 400]
/*
numbers1与numbers两者不存在联系,更改numbers的值不会影响numbers1!
说明copy方法是不会建立两个切片的联系的!
*/
numbers[0] = 666
numbers1[0] = 888
printSlices("numbers",numbers)//numbers addr:0xc000134008 len=4 cap=19 slice=[666 100 300 400]
printSlices("numbers1",numbers1)//numbers1 addr:0xc000136000 len=4 cap=38 slice=[888 100 300 400]
}
append()与copy()的对比案例二***
package main
import (
"fmt"
"strconv"
)
// 输出切片格式化信息
func printSlices(name string,x []string){
fmt.Print(name," ")
fmt.Printf("addr:%p len=%d cap=%d slice=%v \n",x,len(x),cap(x),x)
}
func main(){
// 思考:使用那种初始化切片的方式更高效?
var sa []string
//sa := make([]string,0,10)
printSlices("sa",sa)
// 当使用append追加元素到切片时,如果容量不够,Go会创建一个新的切片变量来存储元素
for i := 0; i < 10; i++{
sa = append(sa,strconv.Itoa(i))
printSlices("sa",sa)
}
printSlices("sa",sa)
}
// 可以看到容量cap的值在增加!
/*
sa addr:0x0 len=0 cap=0 slice=[]
sa addr:0xc00008e1f0 len=1 cap=1 slice=[0]
sa addr:0xc0000a6060 len=2 cap=2 slice=[0 1]
sa addr:0xc0000b2040 len=3 cap=4 slice=[0 1 2]
sa addr:0xc0000b2040 len=4 cap=4 slice=[0 1 2 3]
sa addr:0xc0000bc000 len=5 cap=8 slice=[0 1 2 3 4]
sa addr:0xc0000bc000 len=6 cap=8 slice=[0 1 2 3 4 5]
sa addr:0xc0000bc000 len=7 cap=8 slice=[0 1 2 3 4 5 6]
sa addr:0xc0000bc000 len=8 cap=8 slice=[0 1 2 3 4 5 6 7]
sa addr:0xc0000be000 len=9 cap=16 slice=[0 1 2 3 4 5 6 7 8]
sa addr:0xc0000be000 len=10 cap=16 slice=[0 1 2 3 4 5 6 7 8 9]
sa addr:0xc0000be000 len=10 cap=16 slice=[0 1 2 3 4 5 6 7 8 9]
*/
5.3 map
map的概念
Go 语言提供了内置类型 map,它将一个值与一个键关联起来,可以使用相应的键检索值。这种结构在其他资料中译成地图、映射或字典,但是在Go语言中习惯上翻译成集合。map正如现实生活中的字典一样,使用词-语义进行数据的构建,其中词对应键(key),语义对应值(value),即键与值构成映射的关系,通常将两者称为键值对,这样通过键可以快速找到对应的值。map是一种集合,可以像遍历数组或切片那样去遍历它。因为map是由Hash表实现的,所以对map的读取顺序不固定。
map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取。
map的长度是不固定的,和切片一样可以扩展。内置的len()函数同样适用于map,返回map拥有的键值对的数量。
但是map不能通过cap()函数计算容量(或者说cap()函数的参数不可以是map)。
同一个map中key必须保证唯一。key的数据类型必须是可参与比较运算的类型,也就是支持==或!=操作的类型,如布尔型、整型、浮点型、字符串、数组。切片、函数等引用类型则不能作为key的数据类型。
map的value可以是任何数据类型。map和切片一样,也是一种引用类型。
map的语法 ***
map的声明
可以使用var关键字来定义map,也可以使用内建函数make()。
(1)使用var关键字定义map
var 变量名 map[key类型] value类型
使用var关键字声明map,未初始化的map的默认值是nil。
nil map不能存放键值对。如果要使用map存储键值对,必须在声明时初始化,或者使用make()函数分配到内存空间。
(2)使用make()函数
变量名 := make(map[key类型]value类型)
该声明方式,如果不初始化map,map也不等于nil。
map的初始化赋值和遍历
package main
import (
"fmt"
)
func main(){
// 1. 声明时同时初始化
var country = map[string]string{
"China":"Beijing",
"Japan":"Tokyo",
"USA":"DC",
}
fmt.Println(country) // map[China:Beijing Japan:Tokyo USA:DC]
// 短变量声明初始化方式
fating := map[string]float64{
"a":12.23,
"b":22.24,
"c":34.21,
}
fmt.Println(fating) // map[a:12.23 b:22.24 c:34.21]
// 2. 创建map后再赋值
countryMap := make(map[string]string)
countryMap["China"] = "Beijing"
countryMap["Japan"] = "Tokyo"
countryMap["France"] = "paris"
fmt.Println(countryMap) // map[China:Beijing France:paris Japan:Tokyo]
// 3. 遍历map(无序)
// (1)key、value都遍历
for k, v := range countryMap{
fmt.Println("国家:",k," 首都:",v)
}
/*
国家: China 首都: Beijing
国家: Japan 首都: Tokyo
国家: France 首都: paris
*/
// (2) 只展示key
for k := range countryMap{
fmt.Println(k)
}
// (3)只展示value
for _, value := range countryMap{
fmt.Println(value)
}
}
查看元素是否在map中
可以通过key获取map中对应的value值。语法为:map[key]。当key不存在时,会得到该value值类型的默认值,比如string类型得到空字符串,int类型得到0,程序不会报错。
所以可以通过value, ok := map[key]获知key/value是否存在。ok是bool型,如果ok是true,则该键值对存在,否则不存在。
package main
import (
"fmt"
)
func main(){
var country = map[string]string{
"China":"Beijing",
"Japan":"Tokyo",
"USA":"DC",
}
fmt.Println(country) // map[China:Beijing Japan:Tokyo USA:DC]
// 查看元素是否在map中
query_str := "China"
value, ok := country[query_str]
fmt.Printf("%q\n",value) //""
fmt.Printf("%T , %v\n",ok,ok) //bool , false
if ok{
fmt.Println("找到了!")
}else{
fmt.Println("没找到!")
}
// 或者
if value, ok := country[query_str];ok{
fmt.Println("找到了!,首都:",value)
}else{
fmt.Println("没得!")
}
}
delete()函数
delete(map, key) 函数用于删除集合的某个元素,参数为map和其对应的key。删除函数不返回任何值。
package main
import (
"fmt"
)
func main(){
var country = map[string]string{
"China":"Beijing",
"Japan":"Tokyo",
"USA":"DC",
}
fmt.Println(country) // map[China:Beijing Japan:Tokyo USA:DC]
// 根据key删除map中的某个元素
fmt.Println("删除前:",country) //删除前: map[China:Beijing Japan:Tokyo USA:DC]
if _,ok := country["Japan"];ok{
delete(country,"China")
}
fmt.Println("删除后:",country) //删除后: map[Japan:Tokyo USA:DC]
// 清空map
country = make(map[string]string)
fmt.Println("清空后:",country) //清空后: map[]
}
Go语言没有为map提供清空所有元素的函数,清空map的唯一办法是重新make一个新的map。不用担心垃圾回收的效率,Go语言的垃圾回收比写一个清空函数更高效。
map是引用类型
map与切片相似,都是引用类型。
将一个map赋值给一个新的变量时,它们指向同一块内存(底层数据结构)。因此,修改两个变量的内容都能够引起它们所指向的数据发生变化。
package main
import (
"fmt"
)
func main(){
var country = map[string]string{
"China":"Beijing",
"Japan":"Tokyo",
"USA":"DC",
}
fmt.Println(country) // map[China:Beijing Japan:Tokyo USA:DC]
newCountry := country
newCountry["USA"] = "CCCCCC"
fmt.Println(country) // map[China:Beijing Japan:Tokyo USA:CCCCCC]
fmt.Println(newCountry) // map[China:Beijing Japan:Tokyo USA:CCCCCC]
}
6 Go语言常用内置包
7 Go语言面向对象编程
7.1 Go语言面向对象
其他编程语言大多使用关键字“类”(class)来定义封装对象,表示该类的具体特征,然而Go并不是一个纯面向对象的编程语言。Go语言采用更灵活的“结构体”替代了“类”。
Go语言并没有提供类(class),但是它提供了结构体(struct),方法(method)可以在结构体上添加。与类相似,结构体提供了捆绑数据和方法的行为。
Go语言设计得非常简洁优雅,它没有沿袭传统面向对象编程中的诸多概念,比如继承、虚方法、构造方法和析构方法等。
虽然Go语言没有继承和多态,但是Go语言可以通过匿名字段实现继承,通过接口实现多态。在Go语言中学习面向对象,主要学习结构体(struct)、方法(method)、接口(interface)。
7.2 结构体 ***
单一的数据类型已经满足不了现实开发需求,于是 Go 语言提供了结构体来定义复杂的数据类型。结构体是由一系列相同类型或不同类型的数据构成的数据集合。
结构体的顶定义格式:
type 类型名 struct{
成员属性1 类型1
成员属性2 类型2
成员属性3,成员属性4 类型3
...
}
在使用结构体的过程中注意以下3点:
- 类型名是标识结构体的名称,在同一个包内不能重复。
- 结构体的属性,也叫字段,必须唯一。
- 同类型的成员属性可以写在一行。
结构体的初始化
结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正分配内存。因此只有在定义结构体并实例化后才能使用结构体。
实例化就是根据结构体定义的格式创建一份与格式一致的内存区域。结构体每个实例的内存是完全独立的。
package main
import "fmt"
// 定义Teacher结构体
type Teacher struct{
name string
age int8
sex byte
}
func main() {
// 1. var 声明方式实例化结构体,初始化方式为:对象.属性=值
var t1 Teacher
fmt.Printf("t1>>>%T, %v, %q \n",t1, t1, t1)//main.Teacher, { 0 0}, {"" '\x00' '\x00'}
t1.name = "wanghw"
t1.age = 28
t1.sex = 'M'
fmt.Printf("t1>>>%T, %v, %q \n",t1, t1, t1)//main.Teacher, {wanghw 28 77}, {"wanghw" '\x1c' 'M'}
// 2. 变量简短声明格式实例化结构体,初始化方式为:对象.属性=值
t2 := Teacher{}
t2.name = "Naruto"
t2.age = 22
t2.sex = 'M'
fmt.Printf("t2>>>%T, %v, %q \n",t2, t2, t2)//main.Teacher, {Naruto 22 77}, {"Naruto" '\x16' 'M'}
// 3. 变量简短声明格式实例化结构体,声明时初始化,初始化方式为:属性:值,属性:值可以同行,也可以换行(类似map的用法)
t3 := Teacher{
name: "Sasuke",
age: 23,
sex: 'M',
}
fmt.Printf("t3>>>%T, %v, %q \n",t3, t3, t3)//main.Teacher, {Sasuke 23 77}, {"Sasuke" '\x17' 'M'}
}
使用new()初始化结构体与结构体、数组、切片中的语法糖
语法糖(Syntactic Sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。
通常来说使用语法糖能够提升程序的可读性,从而减少程序代码出错的机会。结构体和数组中都有语法糖。
使用内置函数new()对结构体进行实例化,结构体实例化后形成指针类型的结构体,new()内置函数会分配内存。第一个参数是类型,而不是值,返回的值是指向该类型新分配的零值的指针。该函数用于创建某个类型的指针。
package main
import "fmt"
// 定义Teacher结构体
type Teacher struct{
name string
age int8
sex byte
}
func main() {
// 使用new()内置函数实例化struct
t1 := new(Teacher)
fmt.Printf("t1>>>%T, %v, %q \n",t1, t1, t1)//*main.Teacher, &{ 0 0}, &{"" '\x00' '\x00'}
(*t1).name = "wanghhonwei"
(*t1).age = 22
(*t1).sex = 'M'
fmt.Printf("t1>>>%T, %v, %q \n",t1, t1, t1)//*main.Teacher, &{wanghhonwei 22 77}, &{"wanghhonwei" '\x16' 'M'}
// 语法糖写法
t1.name = "Naruto"
t1.age = 28
t1.sex = 'M'
fmt.Println(t1)//&{Naruto 28 77}
// 数组与切片中的语法糖
SyntacticSugar()
}
func SyntacticSugar(){
// 数组中的语法糖
arr := [4]int{12,13,14,15}
arr2 := &arr
fmt.Println((*arr2)[len(arr)-1]) // 15
fmt.Println(arr2[0])//12
// 切片中的语法糖
arr3 := []int{100,200,300,400}
arr4 := &arr3
fmt.Println((*arr4)[len(arr)-1]) // 400
}
结构体是值类型
结构体作为函数参数,若复制一份传递到函数中,在函数中对参数进行修改,不会影响到实际参数,证明结构体是值类型。
package main
import "fmt"
// 定义Teacher结构体
type Teacher struct{
name string
age int8
sex byte
}
func main() {
// 1、初始化Teacher
t1 := Teacher{"wanghw",22,'M'}
fmt.Printf("t1:%T, %v, %p \n",t1, t1, &t1)//main.Teacher, {wanghw 22 77}, 0xc00005c420
// 2.复制结构体对象
t2 := t1
t2.name = "David"
t2.age = 21
t2.sex = 'M'
fmt.Printf("t2:%T, %v, %p \n",t2, t2, &t2)//main.Teacher, {David 21 77}, 0xc00005a4a0
// 3.结构体对象作为参数传递,修改t2的name ———— 实际上不会改变t2的name的值!
changeName(t2)
// 再打印一下t1 与t2
fmt.Printf("t1:%T, %v, %p \n",t1, t1, &t1)//main.Teacher, {wanghw 22 77}, 0xc00005a420
fmt.Printf("t2:%T, %v, %p \n",t2, t2, &t2)//main.Teacher, {David 21 77}, 0xc00005a4a0
}
func changeName(t Teacher){
t.name = "Sasuke"
fmt.Printf("在changeName函数内部的t:%T, %v, %p \n",t, t, &t)//main.Teacher, {Sasuke 21 77}, 0xc00005a500
}
结构体的深拷贝与浅拷贝
值类型是深拷贝,深拷贝就是为新的对象分配了内存。引用类型是浅拷贝,浅拷贝只是复制了对象的指针。
结构体的拷贝实例:
package main
import "fmt"
// 定义Teacher结构体
type Dog struct {
name string
color string
age int8
kind string
}
func main() {
//1 实现结构体深拷贝
//struct 是值类型,默认复制就是深拷贝
fmt.Println("结构体的深拷贝:")
d1 := Dog{"豆豆","black",2,"二哈"}
fmt.Printf("d1:%T, %v, %p \n",d1, d1, &d1)//main.Dog, {豆豆 black 2 二哈}, 0xc00001c180
d2 := d1 // 深拷贝
fmt.Printf("d2:%T, %v, %p \n",d2, d2, &d2)//main.Dog, {豆豆 black 2 二哈}, 0xc00001c240
d2.name = "毛毛"
fmt.Printf("d2修改后:%T, %v, %p \n",d2, d2, &d2)//main.Dog, {毛毛 black 2 二哈}, 0xc00001c240
fmt.Printf("d1:%T, %v, %p \n",d1, d1, &d1)//main.Dog, {豆豆 black 2 二哈}, 0xc00001c180
//2 实现结构体的浅拷贝(1):直接赋值指针地址
fmt.Println("结构体的浅拷贝(1):")
d3 := &d1
fmt.Printf("d3:%T, %v, %p \n",d3, d3, &d3)//*main.Dog, &{豆豆 black 2 二哈}, 0xc000006030
d3.name = "球球"
d3.age = 1
d3.color = "white"
d3.kind = "萨摩耶"
fmt.Printf("d3修改后:%T, %v, %p \n",d3, d3, &d3)//修改后:*main.Dog, &{球球 white 1 萨摩耶}, 0xc000006030
fmt.Printf("d1:%T, %v, %p \n",d1, d1, &d1)//main.Dog, {球球 white 1 萨摩耶}, 0xc00001c180
//3 实现结构体的浅拷贝(2):通过new()函数来实例化对象
fmt.Println("结构体的浅拷贝(2):")
d4 := new(Dog)
d4.name = "多多"
d4.age = 1
d4.color = "white"
d4.kind = "二哈"
d5 := d4
fmt.Printf("d4:%T, %v, %p \n",d4, d4, &d4)//*main.Dog, &{多多 white 1 二哈}, 0xc000006038
fmt.Printf("d5:%T, %v, %p \n",d5, d5, &d5)//*main.Dog, &{多多 white 1 二哈}, 0xc000006040
// 修改d5
d5.name = "嘻嘻"
d5.color = "black"
fmt.Printf("d5修改后:%T, %v, %p \n",d5, d5, &d5)//*main.Dog, &{嘻嘻 black 1 二哈}, 0xc000006040
fmt.Printf("d4:%T, %v, %p \n",d4, d4, &d4)//*main.Dog, &{嘻嘻 black 1 二哈}, 0xc000006038
}
结构体作为函数的参数及返回值
结构体作为函数的参数及返回值有两种形式:值传递和引用传递:
package main
import "fmt"
type Flower struct{
name, color string
}
func main(){
// 1. 结构体作为参数的用法
f1 := Flower{"玫瑰","红"}
fmt.Printf("f1: %T, %v, %p \n", f1, f1, &f1)
fmt.Println("-----------------------")
// 将结构体对象作为参数
changeInfo1(f1)
fmt.Printf("f1: %T, %v, %p \n", f1, f1, &f1)
fmt.Println("-----------------------")
// 将结构体指针作为参数
changeInfo2(&f1)
fmt.Printf("f1: %T, %v, %p \n", f1, f1, &f1)
fmt.Println("-----------------------")
// 2. 结构体作为返回值的用法
// 结构体对象作为返回值
f2 := getFlower1()
f3 := getFlower1()
fmt.Println("更改前:",f2, f3)
fmt.Printf("f2地址为:%p,f3地址为%p\n",&f2,&f3) // 地址发生变化,对象发生了复制
f2.name = "杏花"
fmt.Println("更改后:",f2, f3)
// 结构体指针作为返回值
f4 := getFlower2()
f5 := getFlower2()
fmt.Println("更改前:",f4, f5)
f4.name = "桃花"
fmt.Println("更改后:",f4, f5)
}
// 返回结构体对象
func getFlower1()(f Flower){
f = Flower{"牡丹","白"}
fmt.Printf("函数 getFlower1内f:%T, %v, %p \n", f, f, &f)
return
}
// 返回结构体指针
func getFlower2()(f *Flower){
//f = &Flower{"芙蓉","红"}
temp := Flower{"芙蓉","红"}
fmt.Printf("函数 getFlower2内temp:%T, %v, %p \n", temp, temp, &temp)
f = &temp
fmt.Printf("函数 getFlower2内f:%T, %v, %p \n", f, f, &f)
return
}
// 传结构体对象
func changeInfo1(f Flower){
f.name = "月季"
f.color = "粉"
fmt.Printf("函数 changeInfo1内f:%T, %v, %p \n", f, f, &f)
}
// 传结构体指针
func changeInfo2(f *Flower){
f.name = "蔷薇"
f.color = "紫"
fmt.Printf("函数 changeInfo2内f:%T, %v, %p \n", f, f, &f)
}
/*
f1: main.Flower, {玫瑰 红}, 0xc00005a420
-----------------------
函数 changeInfo1内f:main.Flower, {月季 粉}, 0xc00005a4a0
f1: main.Flower, {玫瑰 红}, 0xc00005a420
-----------------------
函数 changeInfo2内f:*main.Flower, &{蔷薇 紫}, 0xc000090020
f1: main.Flower, {蔷薇 紫}, 0xc00005a420
-----------------------
函数 getFlower1内f:main.Flower, {牡丹 白}, 0xc00005a5c0
函数 getFlower1内f:main.Flower, {牡丹 白}, 0xc00005a640
更改前: {牡丹 白} {牡丹 白}
f2地址为:0xc00005a5a0,f3地址为0xc00005a620
更改后: {杏花 白} {牡丹 白}
函数 getFlower2内temp:main.Flower, {芙蓉 红}, 0xc00005a720
函数 getFlower2内f:*main.Flower, &{芙蓉 红}, 0xc000090028
函数 getFlower2内temp:main.Flower, {芙蓉 红}, 0xc00005a7a0
函数 getFlower2内f:*main.Flower, &{芙蓉 红}, 0xc000090030
更改前: &{芙蓉 红} &{芙蓉 红}
更改后: &{桃花 红} &{芙蓉 红}
*/
匿名结构体和与结构体的匿名字段
1. 匿名结构体
匿名结构体就是没有名字的结构体,无须通过type关键字定义就可以直接使用。创建匿名结构体时,同时要创建对象。
匿名结构体由结构体定义和键值对初始化两部分组成:
变量名 := struct {
// 定义成员属性
}{
// 初始化成员属性
}
package main
import (
"fmt"
"math"
)
func main(){
// 匿名函数
res := func(a, b float64) float64{
return math.Pow(a,b)
}(2,4)
fmt.Println(res)
// 匿名结构体
addr := struct{
province, city string
}{"四川省","成都市"}
fmt.Println(addr)
cat := struct {
name, color string
age int8
}{name:"花花",color:"black",age:1}
fmt.Println(cat)
}
/*
16
{四川省 成都市}
{花花 black 1}
*/
2. 结构体的匿名字段
匿名字段就是在结构体中的字段没有名字,只包含一个没有字段名的类型。这些字段被称为匿名字段。
如果字段没有名字,那么默认使用类型作为字段名,同一个类型只能有一个匿名字段。结构体嵌套中采用匿名结构体字段可以模拟继承关系。
package main
import "fmt"
type User struct {
string
byte
int8
float64
}
func main(){
// 实例化结构体
user := User{"Steven",'M',35,178.2}
fmt.Println(user)
//依次输出姓名、性别、年龄、身高
fmt.Printf("姓名:%s \n",user.string)
fmt.Printf("性别:%c \n",user.byte)
fmt.Printf("年龄:%d \n",user.int8)
fmt.Printf("身高:%.2f \n",user.float64)
}
/*
{Steven 77 35 178.2}
姓名:Steven
性别:M
年龄:35
身高:178.20
*/
结构体嵌套
将一个结构体作为另一个结构体的属性(字段),这种结构就是结构体嵌套。
结构体嵌套可以模拟面向对象编程中的以下两种关系:
- 聚合关系:一个类作为另一个类的属性。
- 继承关系:一个类作为另一个类的子类。子类和父类的关系。
聚合关系
package main
import "fmt"
type Address struct{
province, city string
}
type Person struct{
name string
age int
address *Address
}
func main() {
// 模拟结构体对象之间的聚合关系
p := Person{}
p.name = "Wanghongwei"
p.age = 22
// 赋值方式1
addr := Address{
"Sichuan","成都",
}
p.address = &addr
fmt.Println(p) // {Wanghongwei 22 0xc00000c080}
fmt.Println("姓名:",p.name,"年龄:",p.age,"省:",p.address.province,"市:",p.address.city)
// 姓名: Wanghongwei 年龄: 22 省: Sichuan 市: 成都
// 修改Person对象的数据,会影响Address的数据!!!
p.address.province = "内蒙古"
p.address.city = "包头市"
// Person的数据
fmt.Println("姓名:",p.name,"年龄:",p.age,"省:",p.address.province,"市:",p.address.city)
// Address的数据
fmt.Println("addr中的省:",addr.province,"市:",addr.city) //addr中的省: 内蒙古 市: 包头市
// 修改Address中的数据,也会影响Person中的数据!!!
addr.province = "广东省"
addr.city = "深圳市"
fmt.Println("姓名:",p.name,"年龄:",p.age,"省:",p.address.province,"市:",p.address.city)
// 姓名: Wanghongwei 年龄: 22 省: 广东省 市: 深圳市
// 赋值方式2
p.address = &Address{
province: "陕西省",
city: "西安市",
}
fmt.Println(p)//{Wanghongwei 22 0xc0000a6060}
fmt.Println("姓名:",p.name,"年龄:",p.age,"省:",p.address.province,"市:",p.address.city)
// 姓名: Wanghongwei 年龄: 22 省: 陕西省 市: 西安市
}
继承关系
继承是传统面向对象编程的三大特征之一,用于描述两个类之间的关系。一个类(子类、派生类)继承自另一个类(父类、超类)。
子类可以有自己的属性和方法,也可以重写父类已有的方法。子类可以直接访问父类所有的属性和方法。
在结构体中,属于匿名结构体的字段称为提升字段,它们可以被访问,匿名结构体就像是该结构体的父类。
采用匿名字段的形式就是模拟继承关系。而模拟聚合关系时一定要采用有名字的结构体作为字段。
接下来通过一个案例来用结构体模拟继承关系:
package main
import "fmt"
type Person struct {
name,gender string
age int
}
type Student struct{
Person
schoolName string
}
func main(){
// 1. 实例化并初始化Person
p1 := Person{"WangHongWei","Male",22}
fmt.Println(p1) //{WangHongWei Male 22}
// 2. 实力话并初始化Student
// 写法1:
s1 := Student{p1,"西南交通大学"}
printInfo(s1)
/*
{{WangHongWei Male 22} 西南交通大学}
{Person:{name:WangHongWei gender:Male age:22} schoolName:西南交通大学}
姓名:WangHongWei, 年龄:22, 性别:Male, 学校:西南交通大学
*/
// 写法2:
s2 := Student{Person{"Josh","Male",23},"Method"}
printInfo(s2)
/*
{{Josh Male 23} Method}
{Person:{name:Josh gender:Male age:23} schoolName:Method}
姓名:Josh, 年龄:23, 性别:Male, 学校:Method
*/
// 写法3:
s3 := Student{
Person: Person{"Naruto","Male",25},
schoolName: "木叶学校",
}
printInfo(s3)
/*
{{Naruto Male 25} 木叶学校}
{Person:{name:Naruto gender:Male age:25} schoolName:木叶学校}
姓名:Naruto, 年龄:25, 性别:Male, 学校:木叶学校
*/
// 写法4:
s4 := Student{}
s4.name = "Sasuke"
s4.gender = "Male"
s4.age = 25
s4.schoolName = "大蛇丸研究室"
printInfo(s4)
/*
{{Sasuke Male 25} 大蛇丸研究室}
{Person:{name:Sasuke gender:Male age:25} schoolName:大蛇丸研究室}
姓名:Sasuke, 年龄:25, 性别:Male, 学校:大蛇丸研究室
*/
}
func printInfo(s Student){
fmt.Println(s)
fmt.Printf("%+v \n", s)
fmt.Printf("姓名:%s, 年龄:%d, 性别:%s, 学校:%s \n",s.name,s.age,s.gender,s.schoolName)
}
尽量避免结构体嵌套时出现相同的成员名
结构体嵌套时,可能存在相同的成员名,成员重名会导致成员名字冲突!需要使用下面方式:
package main
import "fmt"
type A struct {
a, b int
}
type B struct {
a, d int
}
// C继承A与B
type C struct {
A
B
}
func main(){
c := C{}
c.A.a = 1
c.B.a = 2 // 如果调用 c.a = 2 ,会提示 "引起歧义的参数"!!!
c.b = 3
c.d = 4
fmt.Println(c) //{{1 3} {2 4}}
}
当重名时,编译器会报错:Ambiguous reference。
7.3 方法 ***
方法的概念
Go语言同时有函数和方法,方法的本质是函数,但是方法和函数又有所不同。
1. 含义不同
函数(function)是一段具有独立功能的代码,可以被反复多次调用,从而实现代码复用。而方法(method)是一个类的行为功能,只有该类的对象才能调用。
2. 方法有接受者,而函数无接受者
Go语言的方法(method)是一种作用于特定类型变量的函数。这种特定类型变量叫作接受者(receiver)。接受者的概念类似于传统面向对象语言中的this或self关键字。
Go语言的接受者强调了方法具有作用对象,而函数没有作用对象。一个方法就是一个包含了接受者的函数。
Go语言中,接受者可以是结构体,也可以是结构体类型外的其他任何类型。
3. 函数不可以重名,而方法可以重名
只要接受者不同,方法名就可以相同。
基本语法
func (接受者变量 接受者类型) 方法名(参数列表) (返回值列表){
// 方法体
}
接受者在func关键字和方法名之间编写,接受者可以是struct类型或非struct类型,可以是指针类型或非指针类型。
接受者中的变量在命名时,官方建议使用接受者类型的第一个小写字母。下面通过一个案例对比函数和方法在语法上的区别:
package main
import "fmt"
type Employee struct{
name, currency string
salary float64
}
func main(){
emp1 := Employee{"David","$",2000}
// 方法的调用
emp1.printSalary() //员工:David,薪资:$2000.00
// 调用函数
printSalary(emp1) //员工:David,薪资:$2000.00
}
// printSalary 方法
func (e Employee) printSalary(){
fmt.Printf("员工:%s,薪资:%s%.2f \n",e.name,e.currency,e.salary)
}
// printSalary 函数
func printSalary(e Employee){
fmt.Printf("员工:%s,薪资:%s%.2f \n",e.name,e.currency,e.salary)
}
方法和函数
一段程序可以用函数来写,却还要使用方法,主要有以下两个原因:
- Go不是一种纯粹面向对象的编程语言,它不支持类。因此其方法旨在实现类似于类的行为。
- 相同名称的方法可以在不同的类型上定义,而具有相同名称的函数是不允许的。假设有一个正方形和一个圆形,可以分别在正方形和圆形上定义一个名为Area的求取面积的方法。
下面通过一个案例来观察不同的结构体中相同的方法名:
相同方法名:
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func main(){
r1 := Rectangle{10,4}
c1 := Circle{1}
fmt.Println("r1的面积:",r1.Area())
fmt.Println("c1的面积:",c1.Area())
}
// 定义结构体Rectangle的 Area 方法
func (r Rectangle) Area() float64{
return r.width * r.height
}
// 定义结构体Circle的 Area方法
func (c Circle) Area() float64{
return c.radius * c.radius * math.Pi
}
指针作为接受者:
若方法的接受者不是指针,实际只是获取了一个拷贝,而不能真正改变接受者中原来的数据。当指针作为接受者时,情况如下:
package main
import "fmt"
type Rectangle struct {
width, height float64
}
func main() {
r1 := Rectangle{5,8}
r2 := r1
// 打印对象的内存地址
fmt.Printf("r1的地址:%p \n",&r1)//r1的地址:0xc0000b4010
fmt.Printf("r2的地址:%p \n",&r2)//r2的地址:0xc0000b4020
r1.setValue()//setValue方法中r的地址:0xc0000b4040
fmt.Println("r1.height=",r1.height)//8
fmt.Println("r2.height=",r2.height)//8
r1.setValue2()//setValue2方法中r的地址:0xc0000b4010
fmt.Println("r1.height=",r1.height)//666
fmt.Println("r2.height=",r2.height)//8
}
// 将结构体作为接受者 —— 不会改变原始数据
func (r Rectangle) setValue(){
fmt.Printf("setValue方法中r的地址:%p \n",&r)
r.height = 123
}
// 将指针作为接受者 —— 会改变原始数据 —— 注意r本身是一个指针
func (r *Rectangle) setValue2(){
fmt.Printf("setValue2方法中r的地址:%p \n",r)
r.height = 666
}
方法继承
方法是可以继承的,如果匿名字段实现了一个方法,那么包含这个匿名字段的struct也能调用该匿名字段中的方法。
package main
import "fmt"
type Human struct {
name, phone string
age int
}
type Student struct {
Human // 匿名字段
school string
}
type Employee struct {
Human // 匿名字段
company string
}
func main(){
s1 := Student{Human{"David","1553322xxxx",23},"xinan"}
e1 := Employee{Human{"Steven","1323344xxxx",22},"nanxi"}
// 调用父结构体的方法
s1.SayHi()
e1.SayHi()
}
func (h *Human) SayHi(){
fmt.Printf("在Human的SayHi方法中:我是 %s,联系方式是:%s,年龄是:%d \n",h.name,h.phone,h.age)
}
/*
在Human的SayHi方法中:我是 David,联系方式是:1553322xxxx,年龄是:23
在Human的SayHi方法中:我是 Steven,联系方式是:1323344xxxx,年龄是:22
*/
方法重写
在Go语言中,方法重写是指一个包含了匿名字段的struct也实现了该匿名字段实现的方法。
当结构体存在继承关系时,方法调用按照就近原则。
package main
import "fmt"
type Human struct {
name, phone string
age int
}
type Student struct {
Human // 匿名字段
school string
}
type Employee struct {
Human // 匿名字段
company string
}
func main(){
s1 := Student{Human{"David","1553322xxxx",23},"xinan"}
e1 := Employee{Human{"Steven","1323344xxxx",22},"nanxi"}
// 调用父结构体的方法
s1.SayHi()
e1.SayHi()
}
func (h *Human) SayHi(){
fmt.Printf("在Human的SayHi方法中:我是 %s,联系方式是:%s,年龄是:%d \n",h.name,h.phone,h.age)
}
func (s *Student) SayHi(){
fmt.Printf("在Student的SayHi方法中:我是 %s,联系方式是:%s,年龄是:%d \n",s.name,s.phone,s.age)
}
func (e Employee) SayHi(){
fmt.Printf("在Employee的SayHi方法中:我是 %s,联系方式是:%s,年龄是:%d \n",e.name,e.phone,e.age)
}
/*
在Student的SayHi方法中:我是 David,联系方式是:1553322xxxx,年龄是:23
在Employee的SayHi方法中:我是 Steven,联系方式是:1323344xxxx,年龄是:22
*/
7.4 接口 ***
接口的概念
面向对象语言中,接口用于定义对象的行为。接口只指定对象应该做什么,实现这种行为的方式(实现细节)由对象来决定。
在Go语言中,接口是一组方法签名。
接口指定了类型应该具有的方法,类型决定了如何实现这些方法。
当某个类型为接口中的所有方法提供了具体的实现细节时,这个类型就被称为实现了该接口。
接口定义了一组方法,如果某个对象实现了该接口的所有方法,则此对象就实现了该接口。
Go语言的类型都是隐式实现接口的。任何定义了接口中所有方法的类型都被称为隐式地实现了该接口。
接口的定义与实现
定义接口的语法格式如下:
type 接口名 interface {
方法1([参数列表]) [返回值]
方法2([参数列表]) [返回值]
...
方法n([参数列表]) [返回值]
}
实现接口方法的语法格式如下:
func (变量名 结构体类型) 方法1([参数列表]) [返回值] {
// 方法体
}
func (变量名 结构体类型) 方法2([参数列表]) [返回值] {
// 方法体
}
...
func (变量名 结构体类型) 方法n([参数列表]) [返回值] {
// 方法体
}
使用案例如下:
package main
import "fmt"
// interface
type Phone interface {
call()
}
// struct
type AndroidPhone struct {
}
type IPhone struct {
}
// 实现接口
func (a AndroidPhone) call() {
fmt.Println("我是安卓手机,可以打电话了!")
}
func (i IPhone) call(){
fmt.Println("我是苹果手机,可以打电话了!")
}
func main() {
// 定义接口类型的变量
var phone Phone
phone = new(AndroidPhone)
fmt.Printf("%T, %v, %p \n",phone,phone,&phone)
phone.call()
/*
*main.AndroidPhone, &{}, 0xc00008e1e0
我是安卓手机,可以打电话了!
*/
phone = AndroidPhone{}
fmt.Printf("%T, %v, %p \n",phone,phone,&phone)
phone.call()
/*
main.AndroidPhone, {}, 0xc00008e1e0
我是安卓手机,可以打电话了!
*/
phone = new(IPhone)
fmt.Printf("%T, %v, %p \n",phone,phone,&phone)
phone.call()
/*
*main.IPhone, &{}, 0xc00008e1e0
我是苹果手机,可以打电话了!
*/
phone = IPhone{}
fmt.Printf("%T, %v, %p \n",phone,phone,&phone)
phone.call()
/*
main.IPhone, {}, 0xc00008e1e0
我是苹果手机,可以打电话了!
*/
}
实际并没有见到上例中出现安卓手机及苹果手机实现接口Phone的语句。
new(AndroidPhone)以及new(IPhone)可以通过隐式实现接口直接赋值给接口变量phone。
duck typing
![image-20201026142107422](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201026142107422.png)
Go没有implements或extends关键字,这类编程语言叫作duck typing编程语言。
1. 大黄鸭不是鸭子
生活中的大黄鸭,如图7.16所示。
从生物学角度看,鸭子属于脊索动物门、脊椎动物亚门、鸟纲、雁形目。大黄鸭没有生命,所以不是鸭子。
2. duck typing
duck typing是描述事物的外部行为而非内部结构。“一只鸟走起来像鸭子,游泳像鸭子,叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”扩展后,可以将其理解为:“一只鸟看起来像鸭子,那么它就是鸭子。”
duck typing关注的不是对象的类型本身,而是它是如何使用的。
3. duck typing编程语言
使用duck typing的编程语言往往被归类为“动态类型语言”或者“解释型语言”,如Python、Javascript、Ruby等;而非duck typing编程语言往往被归为“静态类型语言”,如C、C++、Java等。
4. 非duck typing编程语言
以Java为例,一个类必须显式地声明“类实现了某个接口”,然后才能用在这个接口可以使用的地方。如果有一个第三方的Java库,这个库中的某个类没有声明它实现了某个接口,那么即使这个类中真的有那些接口中的方法,也不能把这个类的对象用在那些要求用接口的地方。但在duck typing编程语言中就可以这样做,因为它不要求一个类显式地声明它实现了某个接口。
5. 动态类型语言的优缺点
动态类型语言的好处很多,Python代码写起来很快。但是缺陷也是显而易见的:错误往往要在运行时才能被发现。相反,静态类型语言往往在编译时就发现这类错误:如果某个变量的类型没有显式地声明实现了某个接口,那么,这个变量就不能用在一个要求实现了这个接口的地方。
6. Go的处理方式
Go类型系统采取了折中的办法,其做法如下:
- 第一,结构体类型T不需要显式地声明它实现了接口I。只要类型T实现了接口I规定的所有方法,它就自动地实现了接口I。这样就像动态类型语言一样省了很多代码,少了许多限制。
- 第二,将结构体类型的变量显式或者隐式地转换为接口I类型的变量i。这样就可以和其他静态类型语言一样,在编译时检查参数的合法性。
接下来我们通过一个案例加深对duck typing的理解:
package main
import "fmt"
type ISayHello interface {
SayHello() string
}
type Duck struct {
name string
}
type Person struct {
name string
}
func (d Duck) SayHello() string{
return d.name + "叫:ga ga ga!"
}
func (p Person) SayHello() string{
return p.name + "说:你好!"
}
func main() {
// 定义实现接口的对象
duck := Duck{"DaHuangYa"}
person := Person{"Jonson"}
fmt.Println(duck.SayHello())// DaHuangYa叫:ga ga ga!
fmt.Println(person.SayHello())// Jonson说:你好!
// 定义接口类型的变量
var i ISayHello
i = duck
fmt.Printf("%T, %v, %p \n",i, i, &i)//main.Duck, {DaHuangYa}, 0xc00008e200
fmt.Println(i.SayHello())//DaHuangYa叫:ga ga ga!
i = person
fmt.Printf("%T, %v, %p \n",i, i, &i)//main.Person, {Jonson}, 0xc000010220
fmt.Println(i.SayHello())//Jonson说:你好!
}
可以看出,一个函数如果接受接口类型作为参数,那么实际上它可以传入该接口的任意一个实现类的对象作为参数。定义一个接口变量,实际上可以赋值给任意一个实现了该接口的对象。
如果定义了一个接口类型的容器(数组或切片),实际上该容器可以存储任意一个实现类对象。
多态
如果有几个相似而不完全相同的对象,有时人们要求在向它们发出同一个消息时,它们的反应各不相同,分别执行不同的操作。这种情况就是多态现象。例如,甲、乙、丙三个班都是初中一年级,学生们有基本相同的属性和行为,在同时听到上课铃声的时候,他们会分别走向三个不同的教室,而不会走向同一个教室。
多态就是事物的多种形态,Go语言中的多态性是在接口的帮助下实现的——定义接口类型,创建实现该接口的结构体对象。
定义接口类型的对象,可以保存实现该接口的任何类型的值。Go语言接口变量的这个特性实现了Go语言中的多态性。接口类型的对象,不能访问其实现类中的属性字段。
下面通过案例解释多态现象:
package main
import "fmt"
type Income interface {
calculate() float64 // 计算收入总额
source() string // 用来说明收入来源
}
//固定账单项目
type FixedBilling struct {
projectName string // 工程项目
biddedAmount float64 // 项目招标总额
}
//定时生产项目(定时和材料项目)
type TimeAndMaterial struct {
projectName string
workHours float64 // 工作时长
hourlyRate float64 // 每小时效率
}
//固定收入项目
func (f FixedBilling) calculate() float64{
return f.biddedAmount
}
func (f FixedBilling) source() string{
return f.projectName
}
//定时收入项目
func (t TimeAndMaterial) calculate() float64{
return t.workHours * t.hourlyRate
}
func (t TimeAndMaterial) source() string{
return t.projectName
}
// 通过广告点击获得收入
type Advertisement struct {
adName string
clickCount int
incomePerclick float64 // 平均点击量
}
func (a Advertisement) calculate() float64{
return float64(a.clickCount) * a.incomePerclick
}
func (a Advertisement) source() string{
return a.adName
}
func main() {
p1 := FixedBilling{"项目1",5000}
p2 := FixedBilling{"项目2",8000}
p3 := TimeAndMaterial{"项目3",100,40}
p4 := TimeAndMaterial{"项目4",200,30}
p5 := Advertisement{"广告1",10000,0.1}
p6 := Advertisement{"广告2",20000,0.05}
ic := []Income{p1,p2,p3,p4,p5,p6}
fmt.Println("净收入为:",calculateNetIncome(ic))
}
// 计算净收入
func calculateNetIncome(ic []Income) float64{
netincome := 0.0
for _, income := range ic{
fmt.Printf("收入来源:%s, 收入金额:%.2f \n",income.source(),income.calculate())
netincome += income.calculate()
}
return netincome
}
/*
收入来源:项目1, 收入金额:5000.00
收入来源:项目2, 收入金额:8000.00
收入来源:项目3, 收入金额:4000.00
收入来源:项目4, 收入金额:6000.00
收入来源:广告1, 收入金额:1000.00
收入来源:广告2, 收入金额:1000.00
净收入为: 25000
*/
可以看出,尽管添加了新的收入方式,但没有对calculateNetIncome()函数做任何更改,全靠多态性起作用。
由于新的Advertisement类型也实现了Income接口,可以将它添加到ic切片中。
CalculateNetIncome()函数在没有任何更改的情况下工作,因为它可以调用Advertisement类型的calculate()和source()方法。
空接口 ***
空接口中没有任何方法。任意类型都可以实现该接口。空接口这样定义:interface{},也就是包含0个方法(method)的interface。空接口可表示任意数据类型,类似于Java中的object。
空接口常用于以下情形:
- println的参数就是空接口。
- 定义一个map:key是string,value是任意数据类型。
- 定义一个切片,其中存储任意类型的数据!!!
空接口的使用方式:
package main
import "fmt"
type A interface {
}
type Cat struct {
name string
age int
}
type Person struct {
name string
gender string
}
func main() {
var a1 A = Cat{"Mimi",1}
var a2 A = Person{"Steven","Male"}
var a3 A = "Learn golang with me!"
var a4 A = 100
var a5 A = 3.1415926
showInfo(a1)//main.Cat, {Mimi 1}
showInfo(a2)//main.Person, {Steven Male}
showInfo(a3)//string, Learn golang with me!
showInfo(a4)//int, 100
showInfo(a5)//float64, 3.1415926
// 1. fmt.Println() 参数就是空接口
fmt.Println("Pringln的参数就是空接口,可以是任何类型的数据!",100,2.12,Cat{"Mini2",2})
/* Pringln的参数就是空接口,可以是任何类型的数据! 100 2.12 {Mini2 2} */
// 2. 定义map,value是任何类型的数据
map1 := make(map[string]interface{})
map1["name"] = "whw"
map1["age"] = 22
map1["height"] = 1.71
fmt.Println(map1) //map[age:22 height:1.71 name:whw]
// 3.定义一个切片,其中存储任意类型的数据 !!!!!
slice := make([]interface{},0,10)
slice = append(slice,a1,a2,a3,a4,a5)
fmt.Println(slice) //[{Mimi 1} {Steven Male} Learn golang with me! 100 3.1415926]
}
func showInfo(a A){
fmt.Printf("%T, %v \n",a,a)
}
在上例中,变量a1、a2、a3、a4、a5分别为不同类型的变量,它们均可以存放在空接口中使用。
接口对象转型
第一种实现方式
instance, ok := 接口对象.(实际类型)
如果该接口对象是对应的实际类型,那么instance就是转型之后的对象,ok的值为true,配合if ... else if...语句使用。
第二种实现方式
接口对象.[type]
此方式配合switch...case语句使用。
接下来通过一个案例实现接口对象转型(注意三角形Triangle结构体的那个系统接口):
package main
import (
"fmt"
"math"
)
// 1.定义接口
type Shape interface {
perimeter() float64
area() float64
}
// 2.矩形
type Rectangle struct {
a, b float64
}
// 3.三角形
type Triangle struct {
a, b, c float64
}
// 4.圆形
type Circle struct {
radius float64
}
// 定义实现接口的方法
// 矩形的周长与面积
func (r Rectangle) perimeter() float64{
return (r.a + r.b) * 2
}
func (r Rectangle) area() float64 {
return r.a * r.b
}
//三角形的周长与面积
func (t Triangle) perimeter() float64{
return t.a + t.b + t.c
}
func (t Triangle) area() float64{
// 海伦公式
p := t.perimeter() / 2 //半周长
return math.Sqrt(p*(p-t.a) * (p-t.b) * (p-t.c))
}
//圆形的周长与面积
func (c Circle) perimeter() float64{
return 2 * math.Pi * c.radius
}
func (c Circle) area() float64{
return math.Pow(c.radius,2) * math.Pi
}
// 接口对象转型方式1:instance, ok := 接口对象.(实际类型)
func getType(s Shape){
if instance, ok := s.(Rectangle);ok{
fmt.Printf("矩形:长度为%.2f,宽度为%.2f \n",instance.a,instance.b)
}else if instance, ok := s.(Triangle);ok{
fmt.Printf("三角形形:三条边分别为%.2f,%.2f,%.2f \n",instance.a,instance.b,instance.c)
}else if instance, ok := s.(Circle);ok{
fmt.Printf("圆形:半径为%.2f \n",instance.radius)
}
}
// 接口对象转型方式2:接口对象.(type),配合switch...case语句使用
func getType2(s Shape){
switch instance := s.(type) {
case Rectangle:
fmt.Printf("矩形:长度为%.2f,宽度为%.2f \n", instance.a, instance.b)
case Triangle:
fmt.Printf("三角形:三条边分别为%.2f,%.2f,%.2f \n",instance.a,instance.b,instance.c)
case Circle:
fmt.Printf("圆形:半径为%.2f \n",instance.radius)
}
}
func main() {
var s Shape
s = Rectangle{22,33}
getResult(s)
showInfo(s)
s = Triangle{3,4,5}
getResult(s)
showInfo(s)
s = Circle{1}
getResult(s)
showInfo(s)
x := Triangle{3,4,5}
fmt.Println(x)
}
func getResult(s Shape){
//getType(s)
getType2(s)
fmt.Printf("getResult函数中~周长:%.2f,面积:%.2f \n", s.perimeter(),s.area())
}
// 实现了系统接口,最后的打印会改变
func (t Triangle) String() string{
return fmt.Sprintf("Triangle对象,属性分别是:%.2f, %.2f, %.2f ",t.a,t.b,t.c)
}
func showInfo(s Shape){
fmt.Printf("showInfo函数中~%T, %v \n",s, s)
fmt.Println("-----------------------------")
}
/*
矩形:长度为22.00,宽度为33.00
getResult函数中~周长:110.00,面积:726.00
showInfo函数中~main.Rectangle, {22 33}
-----------------------------
三角形:三条边分别为3.00,4.00,5.00
getResult函数中~周长:12.00,面积:6.00
showInfo函数中~main.Triangle, Triangle对象,属性分别是:3.00, 4.00, 5.00
-----------------------------
圆形:半径为1.00
getResult函数中~周长:6.28,面积:3.14
showInfo函数中~main.Circle, {1}
-----------------------------
Triangle对象,属性分别是:3.00, 4.00, 5.00
*/
"面向对象"小结
本章主要讲解了两部分内容。第一部分是面向对象思想以及编写结构体与方法,一个对象其实也就是一个简单的值或者一个变量,这个对象会包含一些方法,而一个方法则是一个和特殊类型关联的函数;第二部分是通过接口实现多态,接口类型是对其他类型行为的抽象和概括,因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式可以让函数更加灵活和更具有适应能力。简单地用面向对象的思想来描述后厨的场景:厨师的职称可以叫作类,雇佣的厨师王师傅叫作对象,身高、体重等叫作属性,做菜叫作方法。多态的意思是,同样是做菜方法,张师傅洗菜,王师傅切菜,李师傅炒菜。值得注意的是,不是任何事物都需要被当成一个对象,独立的函数也有自己的用处。
8 Go语言异常处理
为了保证程序的稳定性、可调试性,为了方便维护者阅读和理解,降低维护成本,Go语言提供了异常处理。
8.1 error ***
error接口
错误是指程序中出现不正常的情况,从而导致程序无法正常运行。假设尝试打开一个文件,文件系统中不存在这个文件。这是一个异常情况,它表示为一个错误。
Go语言通过内置的错误类型提供了非常简单的错误处理机制,即error接口。该接口的定义如下。
type error interface {
Error() string
}
error本质上是一个接口类型,其中包含一个Error()方法,错误值可以存储在变量中,通过函数返回。
它必须是函数返回的最后一个值。
在Go语言中处理错误的方式通常是将返回的错误与nil进行比较。nil值表示没有发生错误,而非nil值表示出现错误。如果不是nil,需打印输出错误。
使用error接口的示例:
package main
import (
"fmt"
"math"
"errors"
"os"
)
func main(){
// 异常情况1
res := math.Sqrt(-100)
fmt.Println(res) // NaN
res, err := Sqrt(-100)
if err != nil{
fmt.Println(err)
}else{
fmt.Println(res)
}
// 异常情况2
res, err = Divide(100,0)
if err != nil{
fmt.Println(err.Error())
}else{
fmt.Println(res)
}
// 异常情况3 打开不存在的文件
f, err := os.Open("./abx.txt")
if err != nil{
fmt.Println(err)
}else{
fmt.Println(f.Name(),"该文件被成功打开!")
}
}
// 定义平方根运算函数
func Sqrt(f float64) (float64, error){
if f < 0{
return 0, errors.New("负数不可以获取平方根")
}else{
return math.Sqrt(f), nil
}
}
// 定义除法运算函数
func Divide(dividee float64, divider float64)(float64, error){
if divider == 0{
return 0, errors.New("除数不可以为0!")
}else{
return dividee / divider, nil
}
}
/*
NaN
负数不可以获取平方根
除数不可以为0!
open ./abx.txt: no such file or directory
*/
创建error对象
结构体只要实现了Error() string这种格式的方法,就代表实现了该错误接口,返回值为错误的具体描述。
通常程序会发生可预知的错误,所以Go语言errors包对外提供了可供用户自定义的方法,errors包下的New()函数返回error对象,errors.New()函数创建新的错误。errors包内代码如下:
![image-20201026164714980](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201026164714980.png)
Go语言的errors.go源码定义了一个结构体,名为errorString,它拥有一个Error()方法,实现了error接口。同时该包向外暴露了一个New()函数,该函数参数为字符串,返回值为error类型。
fmt包下的Errorf()函数返回error对象,它本质上还是调用errors.New()。使用格式如下:
![image-20201026164756916](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201026164756916.png)
接下来通过一个案例演示创建error的方式:
package main
import (
"errors"
"fmt"
)
func main() {
// 1. 创建error对象的方式1
err1 := errors.New("自己创建的错误!")
fmt.Println("err1>>> ",err1)//自己创建的错误!
fmt.Println("err1.Error()>>> ",err1.Error())//自己创建的错误!
fmt.Printf("err1的类型:%T\n ",err1)//*errors.errorString
fmt.Println("----------------")
// 2. 创建error对象的方式2
err2 := fmt.Errorf("错误的类型%d",101)
fmt.Println("err2>>> ",err2)//错误的类型101
fmt.Println("err2.Error()>>> ",err2.Error())//错误的类型101
fmt.Printf("err2的类型:%T\n ",err2)//*errors.errorString
fmt.Println("----------------")
//error对象在函数中的使用
res, err3 := checkAge(-12)
if err3 != nil{
fmt.Println("err3>>> ",err3)//输入的年龄为:-12,是负数!
fmt.Println("err3.Error()>>> ",err3.Error())//输入的年龄为:-12,是负数!
}else{
fmt.Println(res)
}
}
// 设计一个函数验证年龄,如果是负数,则返回error
func checkAge(age int) (string, error){
if age < 0{
err := fmt.Errorf("输入的年龄为:%d,是负数!",age)
return "", err
}else{
return fmt.Sprintf("输入的年龄是:%d",age), nil
}
}
自定义错误 ***
自定义错误的实现步骤如下:
- 定义一个结构体,表示自定义错误的类型。
- 让自定义错误类型实现error接口:Error() string。
- 定义一个返回error的函数。根据程序实际功能而定。
package main
import (
"fmt"
"time"
)
// 1. 定义结构体,表示自定义错误类型
type MyError struct {
When time.Time
What string
}
// 2. 实现Error()方法
func (e MyError) Error() string{
return fmt.Sprintf("%v : %v",e.When, e.What)
}
// 3. 定义函数,返回error对象。该函数求矩形面积
func getArea(width, length float64) (float64, error){
errorInfo := ""
if width < 0 && length < 0{
errorInfo = fmt.Sprintf("长度:%v,宽度:%v 均为负数!",length, width)
} // 省略其他情况的判断!!!
if errorInfo != ""{
return 0, MyError{time.Now(),errorInfo}
}else{
return width * length, nil
}
}
func main() {
res ,err := getArea(-1,-2)
if err != nil{
fmt.Printf(err.Error())
}else{
fmt.Println("面积为:",res)
}
}
// 2020-10-26 17:10:07.162722 +0800 CST m=+0.000098898 : 长度:-2,宽度:-1 均为负数!
8.2 defer ***
关键字defer用于延迟一个函数或者方法(或者当前所创建的匿名函数)的执行。
defer语句只能出现在函数或方法的内部。
函数中使用defer
在函数中可以添加多个defer语句。
如果有很多调用defer,当函数执行到最后时,这些defer语句会按照逆序执行(报错的时候也会执行),最后该函数返回。
defer的执行顺序:
package main
import "fmt"
func main() {
defer funcA()
funcB()
defer funcC()
fmt.Println("main over")
}
func funcA(){
fmt.Println("funcA...")
}
func funcB(){
fmt.Println("funcB...")
}
func funcC(){
fmt.Println("funcC...")
}
/*
funcB...
main over
funcC...
funcA...
*/
defer语句经常被用于处理成对的操作,如打开-关闭、连接-断开连接、加锁-释放锁。
特别是在执行打开资源的操作时,遇到错误需要提前返回,在返回前需要关闭相应的资源,不然很容易造成资源泄露等问题。
package main
import "fmt"
func main() {
s1 := []int{77,88,99,400,234}
getLargest(s1)
}
func finished(){
fmt.Println("结束!")
}
func getLargest(s []int){
// 用defer挂起来
defer finished()
fmt.Println("开始寻找最大的数>>>")
max := 0
for _,v := range s{
if v > max{
max = v
}
}
fmt.Printf("%v 中最大的数为:%v \n",s,max)
}
/*
开始寻找最大的数>>>
[77 88 99 400 234] 中最大的数为:400
结束!
*/
方法中使用defer
延迟并不局限于函数,延迟一个方法调用也是完全合法的。
package main
import "fmt"
type person struct {
firstName, lastName string
}
func (p person) fullName(){
fmt.Printf("%s %s \n",p.firstName,p.lastName)
}
func main() {
p := person{"Steven","Wang"}
// 延迟一个方法
defer p.fullName()
fmt.Println("Welcome!!!")
}
/*
Welcome!!!
Steven Wang
*/
defer参数
延迟函数的参数在执行延迟语句时被执行,而不是在执行实际的函数调用时执行。
package main
import "fmt"
func main() {
a := 5
b := 6
defer printAdd(a, b, true)
a = 10
b = 12
printAdd(a,b,false)
}
func printAdd(a, b int, flag bool){
if flag{
fmt.Printf("延迟执行函数printAdd(),参数a,b分别为:%d,%d \n",a,b)
}else{
fmt.Printf("未延迟执行函数printAdd(),参数a,b为:%d,%d \n",a,b)
}
}
/*
未延迟执行函数printAdd(),参数a,b为:10,12
延迟执行函数printAdd(),参数a,b分别为:5,6
*/
堆栈的推迟
当一个函数有多个延迟调用时,它们被添加到一个堆栈中,并按后进先出(Last InFirst Out,LIFO)的顺序执行。
package main
import "fmt"
func main() {
str := "欢迎学习Golang语言!"
fmt.Printf("原始字符串:%s \n",str)
fmt.Printf("反转后字符串: \n")
ReverseString(str)
}
func ReverseString(str string) {
for _, v := range []rune(str){
defer fmt.Printf("%c",v)
}
}
/*
原始字符串:欢迎学习Golang语言!
反转后字符串:
!言语gnaloG习学迎欢
*/
8.3 panic和recover机制 ***
panic
Go语言追求简洁优雅,Go没有像Java那样的try...catch...finally异常处理机制。Go语言设计者认为,将异常与流程控制混在一起会让代码变得混乱。
panic,让当前的程序进入恐慌,中断程序的执行。panic()是一个内建函数,可以中断原有的控制流程。
panic示例一
package main
import "fmt"
func TestA(){
fmt.Println("func TestA()")
}
func TestB(){
panic("func TestB()")
}
func TestC(){
fmt.Println("func TestC()")
}
func main() {
TestA()
TestB() // TestB()发生异常,中断程序
TestC()
}
/*
func TestA()
panic: func TestB()
goroutine 1 [running]:
main.TestB(...)
/Users/wanghongwei/GoProject/Go语言开发实战/variable.go:10
main.main()
/Users/wanghongwei/GoProject/Go语言开发实战/variable.go:18 +0x96
*/
通常情况下,向程序使用方报告错误状态的方式可以是返回一个额外的error类型值。但是,当遇到不可恢复的错误状态时,如数组访问越界、空指针引用等,这些运行时错误会引起panic异常。这时,上述错误处理方式显然就不适合了。
需要注意的是,不应通过调用panic()函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式。当某些不应该发生的场景发生时调用panic()。
panic示例二:内置的panic()函数引发的panic异常
package main
import "fmt"
func TestA(){
fmt.Println("func TestA()")
}
func TestB(x int){
var a [100] int
a[x] = 1000 // x值为101时,数组越界
}
func TestC(){
fmt.Println("func TestC()")
}
func main() {
TestA()
TestB(101) // TestB()发生异常,中断程序
TestC()
}
/*
func TestA()
panic: runtime error: index out of range [101] with length 100
goroutine 1 [running]:
main.TestB(...)
/Users/wanghongwei/GoProject/Go语言开发实战/variable.go:11
main.main()
/Users/wanghongwei/GoProject/Go语言开发实战/variable.go:19 +0x85
*/
recover
panic异常一旦被引发就会导致程序崩溃。这当然不是程序员愿意看到的,但谁也不能保证程序不会发生任何运行时错误。不过,Go语言为开发者提供了专用于“拦截”运行时panic的内建函数recover()。
recover()可以让进入恐慌流程的Goroutine(可当作线程理解,后续章节会详细讲解)恢复过来并重新获得流程控制权。
需要注意的是,recover()让程序恢复,必须在延迟函数中执行。换言之,recover()仅在延迟函数中有效。
在正常的程序运行过程中,调用 recover()会返回 nil,并且没有其他任何效果。如果当前的Goroutine陷入恐慌,调用recover()可以捕获panic()的输入值,使程序恢复正常运行。
recover()的使用方式:
package main
import "fmt"
func main() {
funcA()
funcB()
funcC()
fmt.Println("main over...")
}
func funcA(){
fmt.Println("这是funcA...")
}
func funcB(){
// defer 一个 匿名函数
defer func(){
if msg := recover();msg != nil{
fmt.Println("恢复啦!获取recover的返回值:",msg)
}
}()
fmt.Println("这是funcB...")
for i := 0; i < 10; i++{
fmt.Println("i:",i)
if i == 5{
panic("funcB恐慌啦!!!")
}
}
}
func funcC(){
// defer 一个 匿名函数
defer func(){
fmt.Println("执行延迟函数...")
msg := recover()
fmt.Println("获取recover的返回值:",msg)
}()
fmt.Println("这是funcC...")
panic("funcC恐慌了!")
}
/*
这是funcA...
这是funcB...
i: 0
i: 1
i: 2
i: 3
i: 4
i: 5
恢复啦!获取recover的返回值: funcB恐慌啦!!!
这是funcC...
执行延迟函数...
获取recover的返回值: funcC恐慌了!
main over...
*/
9 Go语言文件I/O操作
10 Go语言网络编程
10.2 HTTP协议客户端实现
Go语言标准库内置了net/http包,涵盖了HTTP客户端和服务端具体的实现方式。内置的net/http包提供了最简洁的HTTP客户端实现方式,无须借助第三方网络通信库,就可以直接使用HTTP中用得最多的GET和POST方式请求数据。