一脚踢你进Go语言大门!入门者必看,万字长文,建议收藏!
作者:互联网
文章目录
- 第一部分:一脚踢你进Go语言大门!
- 第二部分:Go 的高效并发编程实例
您诸位好啊,我是无尘!
第一部分:一脚踢你进Go语言大门!
Ⅰ、基础不牢,地动山摇
1.第一个例子:Hello World
package main
import "fmt"
func main(){
fmt.Println("Hello World")
}
第一行 package main 代表当前的文件属于哪个包,package 是 go 语言生命包的关键字,main 是包名,main包是一个特殊的包,代表此项目为一个可运行的应用程序,而不是一个被其他项目引用的库。
第二行 import “fmt” 是导入一个 fmt 包,import 是关键字
第三行 func main(){} 定义了一个函数,func 是关键字,main 是函数名,mian 是一个特殊函数,代表整个程序的入口,程序在运行时,会点调用 main 函数。
第四行 fmt.Println(“Hello World”) 是通过 fmt 包的 Println 函数打印 “Hello World”文本。
2.Go 环境搭建
可以从官网 https://golang.org/dl/ (国外官网)和 https://golang.google.cn/dl/ (国内官网)下载Go语言开发包。
2.1环境变量
- GOPATH:Go 项目的工作目录,现在有了 Go Module 模式,所以基本上用来放使用 go get 命令获取的项目
- GOBIN:Go 编译生成的程序安装目录,比如
go install
命令 会把生成的go 程序安装到 GOBIN 目录下,以供终端使用。
-
若工作目录为 /Users/wucs/go,需要把 GOPATH 环境变量设置为 /Users/wucs/go,把 GOBIN 环境变量设置为 $GOPATH/bin
-
Linux/macOS 下,把以下内容添加到 /etc/profile 或者 $HOME/.profile 文件保存即可:
export GOPATH=/Users/wucs/go export GOBIN=$GOPATH/bin
3.项目结构
我们采用 Go Module 模式开进行开发,此模式不必将代码放在GOPATH目录中,可以在任意位置来创建项目。
-
比如项目位置为 \golang\gotour,打开终端,切换到项目目录,然后执行
go mod init example.com/hello
,会生成一个 go.mod 文件。然后在项目根目录创建 main.go 文件。go mod 是Golang 1.11 版本引入的官方包(package)依赖管理工具,用于解决之前没有地方记录依赖包具体版本的问题,方便依赖包的管理。
-
go mod init “module名字”
初始化模块。 -
go mod tidy
增加缺失的包,移除没用的包
-
-
将文章开始的 Hello World 实例写入到 main.go 文件中。
main.go 就是整个项目的入口文件,里面有mian函数。
4.编译发布
-
在项目根目录执行
go build ./main.go
,会在项目根目录生成 main.exe 文件 -
在项目根目录下,终端输入
main
回车,成功打印 “Hello World”,说明程序成功运行。 -
以上生成的可执行文件在项目根目录,也可以把它安装到 $GOBIN目录或者其他任意位置:
go install /main.go
go install 命令可以将程序生成在$GOBIN目录,现在可以在任意位置打开终端,输入mian 回车,都会打印 “Hello World”。
5.跨平台编译
什么是跨平台编译?比如你在windows下开发,可以编译在linux上运行的程序。
Go 语言通过两个环境变量来控制跨平台编译,它们分别是 GOOS 和 GOARCH 。
- GOOS:代表要编译的目标操作系统,常见的有 Linux、Windows、Darwin 等。
- GOARCH:代表要编译的目标处理器架构,常见的有 386、AMD64、ARM64 等
macOS AMD64下开发,编译 linux AMD64 程序:
GOOS=linux GOARCH=amd64 go build ./main.go
关于 GOOS 和 GOARCH 更多的组合,参考官方文档的 $GOOS and $GOARCH 这一节。
Ⅱ、数据类型
1. 都有哪些类型
变量声明
- var 变量名 类型 = 表达式
var i int = 10
- 类型推导
var i = 10
可以根据值的类型来省略变量类型 - 声明多个变量
var (
i int = 0
k int = 1
)
// 同理类型推导
var (
i = 0
k = 1
)
类型int/float64/bool/string 等基础类型都可以被自动推导。
整型
在 Go 语言中,整型分为:
- 有符号整型:int、int8、int16、int32、int64
- 无符号整型:uint、uint8、uint16、uint32、uint64
注意:
- 有符号整型可以表示负数、零、正数,而无符号整型只能为零和正数。
- int 和 uint 这两个没有具体的 bit 大小的整型,他们大小可能是32bit,也可能是64bit,这个取决于硬件设备CPU。
- 在整型中,如果能确定int的bit就使用明确的int类型,这一有助于程序的移植性。
- 还有一种字节类型 byte,它其实等价于 uint8,可以理解为 uint8 类型的别名,用于定义一个字节,所以字节byte类型也属于整型。
浮点数
浮点数就是含有小数的数字,Go语言中提供了两种精度的浮点数:float32、float64。因为 float64 精度高,浮点计算结果比 float误差要更小,所以它更被常使用。
布尔型
- 一个布尔值值只有两种:true 和 false。
- 定义使用:
var bf bool = false
;使用 bool 关键字定义
字符串
字符串通过类型 string 声明
var s1 string = "hello"
var s2 = "world" //类型推导
var s3 = s1 + s2 //可以通过操作符 + 把字符串串连起来
s1 += s2 //也可以通过 += 运算符操作
零值
零值其实就是一个变量的默认值,Go语言中,如果只声明了一个变量,并没有对其赋值,那么此变量会有一个对应类型的零值。
var b bool // bool型零值是false
var s string // string的零值是""
以下六种类型零值常量都是nil
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error是接口
2.变量的简短声明
变量名:=表达式
在实际项目中,如果能为声明的变量初始化,那么就使用简短的声明方式,这种也是使用最多的。
3.指针
Go 语言中,指针对应的是变量在内存中存储的位置,也就是说指针的值就是遍历的内存地址。通过 & 可以获取变量的地址,也就是指针。*可以获取地址对应的值。
pi:=&i
fmt.Println(*pi)
4.常量
常量的值是在编译期就确定好的,确定后不能被修改,可以防止在运行期被恶意篡改。
常量定义
和变量类型,只不过使用关键字 const
const name = "无尘"
在 Go语言中,只允许布尔型、字符串、数字类型这些基础类型作为常量。
5.iota
iota 是一个常量生成器,可以用来初始化相似规则的常量,避免重复的初始化。
const (
one = 1
two = 2
three = 3
)
//使用 iota
const (
one = iota+1
two
three
)
iota 的初始值是0。
6.字符串
- 字符串和数字互换
Go是强类型语言,不同类型的变量是不能相互使用和计算的。不同类型的变量在进行复制或计算时,需要先进行类型转换。
i := 10
itos := strconv.Itoa(i)
stoi,err := strconv.Atoi(itos)
fmt.Println(itos,stoi,err) //10 10 nil
- String 包
string 包是Go SDK提供的一个标准包。用于处理字符串的工具包。包含查找字符串、拆分字符串、去除字符串的空格、判断字符串是否含有某个前缀或后缀。
//判断s1的前缀是否是H
fmt.Println(strings.HasPrefix(s1,"H"))
//在s1中查找字符串o
fmt.Println(strings.Index(s1,"o"))
//把s1全部转为大写
fmt.Println(strings.ToUpper(s1))
更多例子,可以查看 string文档
Ⅲ、控制结构
1. if 条件语句
func main() {
i:=6
if i >10 {
fmt.Println("i>10")
} else if i>5 && i<=10 {
fmt.Println("5<i<=10")
} else {
fmt.Println("i<=5")
}
}
注意:
- if 后的表达无 ‘( )’
- 每个条件分支中的 ‘{ }’ 是必须的。哪怕只有一行代码。
3.if/else后的 ‘{’ 不能独占一行。否则编译不通过
2. switch 选择语句
if 条件语句比较适合分支比较少的情况。如果有很多分支,switch会更方便。
switch i:=6;{
case i > 10:
fmt.Println("i>10")
case i > 6 && i <= 10:
fmt.Println("5<i<10")
default:
fmt.Println("i<=5")
}
注意: Go 语言为防止忘记写 break,case 后自带 break,这和其他语言不一样。
如果确实需要执行下一个 case ,可以使用 fallthrough 关键字
switch j:=1;j{
case 1:
fallthrough
case 2:
fmt.Println("1")
default:
fmt.Println("无匹配")
}
以上结果会输出 1。
当switch之后有表达式时,case后的值就要和这个表达式的结果类型相同,比如这里 j 是 int 类型,所以 case 后就得使用 int 类型。
3. for 循环语句
for 循环由三部分组成,其中需要使用两个 ; 分割:
sum := 0
for i := 1; i <= 100; i++{
sum += i
}
fmt.Println("sum:",sum)
第一部分是简单语句
第二部分是 for 循环的条件
第三部分是更新语句
这三部分组成都不是必须的,可以被省略。
Go 语言中没有 while 循环,可以通过for达到while的效果:
sum := 0
i := 1
for i <= 100 {
sum += 1
i++
}
Go 中,同样支持continue,break 控制for循环。
- continue 跳出本次循环,进入下次循环。
- break 强行退出整个循环。
Ⅳ、集合类型
1. Array(数组)
数组存放的是固定长度、相同类型的数据。
1.1数组声明
-
var <数组名> = [<长度>]<元素>{元素1,元素2}
var arr = [2]int{1,2}
或者
arr := [2]int{1,2}
-
var <数组名> = […]<元素类型>{元素1,元素2}
var arr = [...]int{1,2}
或者
arr := [...]int{1,2}
-
var <数组名> = […]<类型>{索引1:元素1,索引2:元素2}
var arr = [...]int{1:1,0:2}
或者
arr := [...]int{1:1,0:2}
数组的每个元素在内存中都是连续存放的,每个元素都有一个下标,下标从0开始。
数组长度可以省略,会自动根据{}中的元素来进行推导。
没有初始化的索引,默认值是数组类型的零值。
1.2 数组循环
for i,v := range array {
fmt.Printf("索引:%d,值:%s\n",i,v)
}
- range 表达式返回数组索引赋值给 i,返回数组值赋值给 v。
- 如果返回的值用不到,可以用 _ 下划线丢弃:
for _,v:= range array{
fmt.Printf("值:%s\n",i,v)
}
2. 切片
切片和数组类型,可以理解为动态的数组,切片是基于数组实现的,它的底层就是一个数组。对于数组的分割,便可以得到一个切片。
2.1数组生成切片
slice := array[start:end]
array := [5]string{"a","b","c","d","e"}
slice := array[2:5]
fmt.Println(slice) //[c d e]
注意:这里包含索引2,但是不包含索引5的元素,即:左闭右开。
经过切片后,切片的索引范围也改变了。
array[start:end] 中的 start 和 end 都是可以省略的,start 的默认值是 0 ,end 的默认值为数组的长度。
array[:] 等价于 array[0:5]
2.2 切片修改
切片的值也可以被修改,这里也可以证明切片的底层是数组。
array := [5]string{"a","b","c","d","e"}
slice := array[2:5] //[c d e]
slice[1] = "f"
fmt.Println(slice) //[c f e]
fmt.Println(array) //[a b c f e]
修改切片,对应的数组值也被修改了,所以证明基于数组的切片,使用的底层数组还是原来的数组,一旦修改切片的元素值,底层数组对应的值也会被修改。
2.3切片声明
使用 make 函数声明切片
//声明一个元素类型为string的切片,长度是4
slice := make([]string,4)
//长度是4,容量是8
slice1 := make([]srting,4,8)
切片的容量不能比切片长度小。
长度就是元素个数。
容量就是切片的空间。
上面实例在内存上划分了一个容量为8的内存空间,但是只是用了4个内存空间,剩余的处于空闲状态。当通过 append 往切片追加元素时,会追加到空闲内存上,剩余空间不足时,会进行扩容。
字面量初始化切片
slice2 := []string{"a","b","c"}
fmt.Println(len(slice2),cap(slice2)) //3 3
2.3 Append
append 函数对一个切片进行追加元素:
slice3 := append(slice2,"d")
//追加多个元素
slice3 := append(slice2,"d","f")
//追加一个切片
slice3 := append(slice2,slice...)
小技巧:
在创建新切片时,最好让长度和容量一样,这样追加操作的时候就会生成新的底层数组,从而和原有数组分离,就不会因为公用底层数组导致修改内容的时候影响多个切片。
2.4切片循环
切片循环与数组一样,也是使用 for range 方式。
3. Map (映射)
map 是一个无序的 k-v 键值对集合。其中 k 必须是相同类型。k 和 v 的类型可以不同。 k 的类型必须支持 == 比较运算符,这样才可以判断它是否存在,并保证唯一。
3.1 Map 声明初始化
-
make:
mapName := make(map[string]int)
-
字面量:
mapName := map[string]int{"无尘":29}
如果不想创建的时候添加键值对,使用空大括号{}即可,切记不能省略。
3.2 Map 获取、删除
//添加键值对或更新对应的key的value
mapName["无尘"] = 20
//获取指定key的value
age := mapName["无尘"]
获取不存在的 k-v 键值对时,如果 key 不存在,返回的 value 是该值的零值,所以很多时候,需要先判断 map 中的 key 是否存在。
nameAge := make([string]int)
nameAge["无尘"]=29
age,ok := nameAge["无尘"]
if ok {
fmt.Println(age)
}
- map 的 [] 操作返回两个值
- 第一个是 value
- 第二个是标记该 key 是否存在,存在则为 true
delete()函数进行删除
delete(nameAge,"无尘")
- delete 有两个参数,一个是map,一个是要删除的 key 。
4. 遍历 Map
nameAge["无尘"] = 29
nameAge["无尘1"] = 30
nameAge["无尘2"] = 31
for k,v := range nameAge{
fmt.Println("key is",k,"value is ",v)
}
- 对应 map ,for range 返回两个参数,分别是 k 和 v。
小技巧:for range 遍历 map 的时候,若使用一个返回值,则这个返回值是 map 的 key 。
4.1 Map 的大小
map 不同于切片,map 只有长度,没有容量。可以使用 len 函数获取 map 大小。
5. String 和 []byte
字符串也是一个不可变的字节序列,可以直接转为字节切片 []byte :
s:="Hello无尘小生"
bs := []byte(s)
string 不止可以直接转为 []byte,还可以使用 [] 操作符获取指定索引的字节值。
字符串是字节序列,每一个索引对应一个字节,在 UTF8 编码下,一个汉字对应三个字节。
如果把一个汉字当做一个长度计算,可以使用 utf8.RuneCountInString 函数。
for range 遍历时,是按照 unicode 字符进行循环的,一个汉字占一个长度。
Ⅴ、函数和方法
1. 函数
1.1 函数声明
func funcName(params) result {
body
}
- 关键字 func 用于声明一个函数
- funcName 函数名
- params 函数的参数
- result 是函数的返回值,可以返回多个返回值,如果没有可以省略。
- body 函数体
示例
1.
- a、b形参类型一致,可以省略其中一个类型的声明
func sum (a, b int) {
return a + b
}
2.多值返回
- 返回值的部分类型定义需要小括号括起来。
func sum (a, b int) (int,error) {
if a <0 || b <0 {
return 0, errors.New("a或b不能是负数")
}
return a + b, nil
}
3.命名参数返回
- 函数中给命名返回参数赋值,相当于函数有了返回值,所以可以忽略 return 后要返回的值了。
func sum (a, b int) (sum int,err error) {
if a <0 || b <0 {
return 0, errors.New("a或b不能是负数")
}
sum = a + b
err = nil
return
}
4.可变参数
- 函数的参数是可变的
- 定义可变参数,只要在参数类型前加三个点 … 即可
- 可变参数的类型其实就是切片,下面示例中 params 的参数类型是 []int
func sum(params ...int) int {
sum := 0
for _, i := range params {
sum += i
}
return sum
}
1.2 包级函数
- 函数都会从属于一个包,我们自定义的函数属于 main 包。Println 函数属于 fmt 包。
- 想要调用其他包内的函数,那么那个函数名称首字母要大写,使其作用域变为公有的。
- 函数首字母小写,只能在同一个包中被调用
1.3 匿名函数和闭包
匿名函数就是没有名称的函数。
func main(){
//注意,sum 只是一个函数类型的变量,不是函数名字
sum := func(a, b int) int {
return a + b
}
fmt.Println(sum(1, 2)) // 3
}
匿名函数可以在函数中进行嵌套,这个匿名函数称为内部函数,内部函数可以使用外部函数的变量,这种方式就是闭包。
func main (){
sm := sum()
fmt.Println(sum())
fmt.Println(sum())
fmt.Println(sum())
}
func sum () func() int{
i := 0
return func ()int{
i++
return i
}
}
//结果为:
1
2
3
由于闭包函数,sum 函数返回一个匿名函数,匿名函数持有外部函数 sum 的变量 i,所以在main函数中,每次调用 sum(),i的值就会 +1。
在 Go 语言中,函数也是一种类型,可以作为函数类型的变量、参数、或者一个函数的返回值。
2. 方法
方法和函数类似,不同之处就是方法必须有一个接收者,这个接收者是一个“类”(类型),这样这个方法就算属于这个“类”。
type Name string
func (n Name)String(){
fmt.Println("name is ", n)
}
- 示例中 String() 就是 Name 这个类型的方法
- 接收者需要加在 func 和方法名之间,使用()
- 接收者: (变量,类型)
使用:
func main(){
name := Name("无尘")
name.String()
}
//出处
name is 无尘
3. 值类型接收者、指针类型接收者
方法的接收者可以使用值类型(例如上面示例)或者指针类型。
如果接收者是指针,那么对指针的修改是有效的:
func (n *Name) Modify(){
*n = Name("wucs")
}
func main(){
name := Name("无尘")
name.String()
name.Modify()
name.String()
}
//输出
name is 无尘
name is wucs
注意:在调用方法时,传递的接收者实质上都是副本,只不过一个是值副本,一个是指向这个值的指针的副本。指针指向原有值,所以修改指针指向的值,也就修改了原有值。
方法的调用者,可以是值,也可以是指针((&name).Modify()),Go 语言会自动转义,我们无需关心。
4. 方法表达式
方法可以赋值给变量
name := Name("无尘")
//方法赋值给变量,方法表达式
n = Name.String
//要传一个接收者name进行调用
n(name)
无论方法是否有参数,通过方法表达式调用,第一个参数必须是接收者,然后才是方法自身的参数。
Ⅵ、struct 和 interface
1. 结构体
1.1 定义
结构体是种聚合类型,里面可以包含任意类型的值,这些值就是结构体的成员,或成为字段,定义结构体,需要使用 type+struct 关键字组合
type person struct { //人结构体
name string //人的名字
age uint //人的年龄
}
- type 与 struct 是关键字,用来定义一个新结构体的类型。
- person 为结构体名字。
- name/age 为结构体的字段名,后面指对应的字段类型。
- 字段声明和变量类似,变量名在前,类型在后
- 字段可以是人一个,一个字段都没有的结构体,成为空结构体。
- 结构体也是一种类型,比如 person 结构体和 person 类型是一个意思。
1.2 声明
- 像普通字符串、整型医院声明初始化
var p person
声明了一个person类型的变量p,但是没有初始化,所以默认使用结构体里字段的零值。
- 字面量方式初始化
p := person{"无尘",18}
表示结构体变量 p 的name字段初始化为“无尘”,age字段初始化为18。顺序必须和字段定义顺序一致。
- 根据字段名称初始化
p := person{age:18,name:"无尘"}
像这样指出字段名,就可以打乱初始化字段的顺序。也可以只初始化其中部分字段,剩余字段默认使用零值:
p := person{age:30}
1.3字段结构体
结构体字段可以是任意类型,包括自定义的结构体类型:
type person struct { //人结构体
name string
age uint
addr address //使用自定义结构体类型
}
type address struct { //地址结构体
city string
}
对于这样嵌套结构体,初始化和一般结构体类似,根据字段对应的类型初始化即可:
p := person {
age:18,
name:"无尘",
addr:address{
city:"北京",
},
}
结构体的字段和调用一个类型的方法一样,都是使用点操作符“.”:
fmt.Println(p.age)
//访问嵌套结构体里的city字段的值:
fmt.Println(p.addr.city)
2. 接口
2.1 定义
接口是一个抽象的类型,是和调用方的一种约定。接口只需要定义约定,告诉掉用方可以做什么,而不用知道它的内部实现。
接口的定义是 type + interface关键字类实现。
//Info 是一个接口,它有方法 Getinfo()string
type Info interface {
Getinfo() string
}
对应 Stringer 接口,它会告诉调用者可以通过 String()放获取一个字符串,这就是接口的约定,而这个字符串是怎么获取到的,接口并不关心,调用者也不用关心,因为这些是接口的实现者来处理的。
2.2 接口的实现
接口的实现者必须是一个具体的类型:
func (p person) Getinfo() string {
return fmt.Sprintf("my name is %s,age is %d",p.name,p.age)
}
- 给结构体类型 person 定义了一个方法,这个方法和接口里的方法名称、参数、返回值都一样,就表示这个结构体 person 实现了 Info 接口。
- 如果一个接口有多个方法,那么要实现接口中的所有方法才算是实现了这个接口。
2.3 使用
我们先定义一个可以打印 Info 接口的函数:
func printInfo(i Info) {
fmt.Println(i.Getinfo())
}
- 定义函数 pringInfo,它接收一个 Info 接口类型的参数,然后打印接口 Getinfo 方法返回的字符串。
- 这个 pringInfo 函数此处是面向接口编程,只有任何一个类型实现了Info接口,都可以使用这个函数打印出对应的字符串,而不用关心具体的类型实现。
printInfo(p)
//结果为:my name is 无尘,age is 18
因为 person 类型实现了Info接口,所以变量p可以作为函数printInfo的参数。
3. 值接受者、指针接受者
- 实现一个接口,必须实现接口中所有的方法。
- 定义一个方法,有值类型接收者和指针类型接收者,两者都可以调用方法,因为Go编译器自动做了转换。
- 但是接口的实现,值类型接收者和指针类型接收者不一样
上面接口体person实现了Info接口,是否结构体指针也实现了该接口呢?
printInfo(&p)
测试发现p的指针作为参数函数也是可以正常运行,表明以值类型接收者实现接口,类型本身和该类型的指针类型,都实现了该接口
那么把接收者改成指针类型:
func (p *person) Getinfo() string {
return fmt.Sprintf("my name is %s,age is %d",p.name,p.age)
}
然后再调用函数 printInfo(p)
,代码编译不通过,表明以指针类型接收者实现接口,只有对应的指针类型才被认为实现了接口
方法接收者 | 实现接口类型 |
---|---|
(p person) | person和*person |
(p *person) | *person |
- 当值类型作为接收者,person类型和*person类型都实现了该接口。
- 当指针类型作为接收者,只有 *person类型实现了该接口。
Ⅶ、错误处理,error 和 panic
1. 错误
在Go语言中,错误并不是非常严重,它是可以预期的,可以返回错误给调用者自行处理。
1.1 error 接口
在Go语言中,错误是通过内置的error接口来表示的,它只有一个Error方法来返回错误信息:
type error interface {
Error() string
}
这里演示一个错误的示例:
func main() {
i,err := strconv.Atoi("a")
if err != nil {
fmt.Println(err)
}else {
fmt.Println(i)
}
}
- 示例故意使用错误的字符串“a”来转为整数,所以这里会打印错误信息:
strconv.Atoi: parsing "a": invalid syntax
- 一般,error接口在当函数或方法调用时遇到错误时进行返回,且为第二个返回值,这样调用者就可以根据错误来自行处理。
1.2 error 工厂函数
我们可以使用 errors.New 这个工厂函数来生成错误信息,它接收一个字符串参数,返回一个error接口。
func test(m,n int) (int, error) {
if m > n {
return m,errors.New("m大于n")
}else {
return n,nil
}
}
当m大约n的情况下,返回一个错误信息。
1.3 自定义 error
上面工厂函数只能传递一个字符串来返回,要想携带更多信息,这时候可以使用自定义error:
type testError struct {
errorCode int //错误码
errorMsg string //错误信息
}
func (t *testError) Error() string{
return t.errorMsg
}
这里自定义error,它可以返回更多信息:
return m, &testError{
errorCode: 1,
errorMsg: "m大于n"}
上面通过字面量方式创建*testError 来返回。
1.4 error 断言
通过error断言来获取返回的错误信息,断言可以将error接口转为自己定义的错误类型:
res, err := test(2,1)
if e,ok := err.(*testError);ok {
fmt.Println("错误码:",e.errorCode,",错误信息:",e.errorMsg)
} else {
fmt.Println(res)
}
2. Panic 异常
Go语言是一门静态语言,很多错误可以在编译的时候进行捕获,不过对于数组越界访问、不同类型强制转换这种,会在运行时候才会引起panic异常。
我们也可以手动来抛出 panic 异常,这里以连接mysql数据库为例:
func connectMySQL(ip,username,password string){
if ip =="" {
panic("ip不能为空")
}
//省略其他代码
}
- 在以上函数中,如果ip地址为空,会抛出 panic 异常。
- panic 是Go语言内置函数,可以接收 interface{} 类型的参数,也就是说任何类型的值都是可以传递给 panic 函数的:
func panic(v interface{})
interface{} 表示空接口,代表任意类型。
panic 是一种非常严重的错误,会使程序中断执行,所以 如果不是影响程序运行的错误,使用 error 即可
2.1 Recover 捕获 Panic 异常
一般我们不对panic异常做处理,但是如果有一些需要在程序崩溃前做处理的操作,可以使用内置的 recover 函数来恢复 panic 异常。
程序 panic 异常崩溃的时候,只有defer修饰的函数才会被执行,所以 recover 函数要结合 defer 关键字一起使用:
func main() {
defer func() {
if p:=recover();p!=nil{
fmt.Println(p)
}
}()
connectMySQL("","root","123456")
}
recover 函数捕获了 panic 异常,打印:recover 函数返回的值就是通过 panic 函数传递的参数值。 ip不能为空
- recover 函数的返回值就是 panic 函数传递的参数值。
- defer 关键字修饰的函数,会在主函数退出前被执行。
Ⅷ、断言和反射
1. 接口断言
提到接口断言,我们先回顾下怎么实现接口?
- 接口的实现者必须是一个具体类型
- 类型定义的方法和接口里方法名、参数、返回值都必须一致
- 若接口有多个方法,那么要实现接口中的所有方法
对于空接口 interface{} ,因为它没有定义任何的函数(方法),所以说Go中的所有类型都实现了空接口。
当一个函数的形参是 interface{} 时,意味着这个参数被自动的转为interface{} 类型,在函数中,如果想得到参数的真实类型,就需要对形参进行断言。
- 类型断言就是将接口类型的值x,转换成类型T,格式为:x.(T)
- 类型断言x必须为接口类型
- T可以是非接口类型,若想断言合法,则T必须实现x的接口
1.1 语法格式:
//非安全类型断言
<目标类型的值> := <表达式>.( 目标类型 )
// 安全类型断言
<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 )
示例
package main
import "fmt"
func whoAmi(a interface{}) {
//1.不断言
//程序报错:cannot convert a (type interface{}) to type string: need type assertion
//fmt.Println(string(a))
//2.非安全类型断言
//fmt.Println(a.(string)) //无尘
//3.安全类型断言
value, ok := a.(string) //安全,断言失败,也不会panic,只是ok的值为false
if !ok {
fmt.Println("断言失败")
return
}
fmt.Println(value) //无尘
}
func main() {
str := "无尘"
whoAmi(str)
}
断言还有一种形式,就是使用switch语句判断接口的类型:
func whoAmi(a interface{}) {
switch a.(type) {
case bool:
fmt.Printf("boolean: %t\n", a) // a has type bool
case int:
fmt.Printf("integer: %d\n", a) // a has type int
case string:
fmt.Printf("string: %s\n", a) // a has type string
default:
fmt.Printf("unexpected type %T", a) // %T prints whatever type a has
}
}
2. 反射
Go语言提供了一种机制,在运行时可以更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。
2.1 反射有何用
- 上面我们提到空接口,它能接收任何东西
- 但是怎么来判断空接口变量存储的是什么类型呢?上面介绍的类型断言可以实现
- 如果想获取存储变量的类型信息和值信息就需要使用到反射
- 反射就是可以动态获取变量类型信息和值信息的机制
2.1 reflect 包
反射是由reflect包来提供支持的,它提供两种类型来访问接口变量的内容,即Type 和 Value。
reflect包提供了两个函数来获取任意对象的Type 和 Value:
- func TypeOf(i interface{}) Type
- func ValueOf(i interface{}) Value
函数 | 作用 |
---|---|
reflect.TypeOf() | 获取变量的类型信息,如果为空则返回nil |
reflect.ValueOf() | 获取数据的值,如果为空则返回0 |
示例:
package main
import (
"fmt"
"reflect"
)
func main() {
var name string = "微客鸟窝"
// TypeOf会返回变量的类型,比如int/float/struct/指针等
reflectType := reflect.TypeOf(name)
// valueOf返回变量的的值,此处为"微客鸟窝"
reflectValue := reflect.ValueOf(name)
fmt.Println("type: ", reflectType) //type: string
fmt.Println("value: ", reflectValue) //value: 微客鸟窝
}
- 函数 TypeOf 的返回值 reflect.Type 实际上是一个接口,定义了很多方法来获取类型相关的信息:
type Type interface {
// 所有的类型都可以调用下面这些函数
// 此类型的变量对齐后所占用的字节数
Align() int
// 如果是 struct 的字段,对齐后占用的字节数
FieldAlign() int
// 返回类型方法集里的第 `i` (传入的参数)个方法
Method(int) Method
// 通过名称获取方法
MethodByName(string) (Method, bool)
// 获取类型方法集里导出的方法个数
NumMethod() int
// 类型名称
Name() string
// 返回类型所在的路径,如:encoding/base64
PkgPath() string
// 返回类型的大小,和 unsafe.Sizeof 功能类似
Size() uintptr
// 返回类型的字符串表示形式
String() string
// 返回类型的类型值
Kind() Kind
// 类型是否实现了接口 u
Implements(u Type) bool
// 是否可以赋值给 u
AssignableTo(u Type) bool
// 是否可以类型转换成 u
ConvertibleTo(u Type) bool
// 类型是否可以比较
Comparable() bool
// 下面这些函数只有特定类型可以调用
// 如:Key, Elem 两个方法就只能是 Map 类型才能调用
// 类型所占据的位数
Bits() int
// 返回通道的方向,只能是 chan 类型调用
ChanDir() ChanDir
// 返回类型是否是可变参数,只能是 func 类型调用
// 比如 t 是类型 func(x int, y ... float64)
// 那么 t.IsVariadic() == true
IsVariadic() bool
// 返回内部子元素类型,只能由类型 Array, Chan, Map, Ptr, or Slice 调用
Elem() Type
// 返回结构体类型的第 i 个字段,只能是结构体类型调用
// 如果 i 超过了总字段数,就会 panic
Field(i int) StructField
// 返回嵌套的结构体的字段
FieldByIndex(index []int) StructField
// 通过字段名称获取字段
FieldByName(name string) (StructField, bool)
// FieldByNameFunc returns the struct field with a name
// 返回名称符合 func 函数的字段
FieldByNameFunc(match func(string) bool) (StructField, bool)
// 获取函数类型的第 i 个参数的类型
In(i int) Type
// 返回 map 的 key 类型,只能由类型 map 调用
Key() Type
// 返回 Array 的长度,只能由类型 Array 调用
Len() int
// 返回类型字段的数量,只能由类型 Struct 调用
NumField() int
// 返回函数类型的输入参数个数
NumIn() int
// 返回函数类型的返回值个数
NumOut() int
// 返回函数类型的第 i 个值的类型
Out(i int) Type
// 返回类型结构体的相同部分
common() *rtype
// 返回类型结构体的不同部分
uncommon() *uncommonType
}
- 函数 TypeOf 的返回值 reflect.Value 是一个结构体类型。Value 结构体定义了很多方法,通过这些方法可以直接操作 Value 字段 ptr 所指向的实际数据:
// 设置切片的 len 字段,如果类型不是切片,就会panic
func (v Value) SetLen(n int)
// 设置切片的 cap 字段
func (v Value) SetCap(n int)
// 设置字典的 kv
func (v Value) SetMapIndex(key, val Value)
// 返回切片、字符串、数组的索引 i 处的值
func (v Value) Index(i int) Value
// 根据名称获取结构体的内部字段值
func (v Value) FieldByName(name string) Value
// ……
struct反射示例:
package main
import (
"fmt"
"reflect"
)
type Address struct {
City string
}
type Person struct {
Name string
Age uint
Address // 匿名字段
}
func (p Person) Hello(){
fmt.Println("我是无尘啊")
}
func main() {
//p := Person{Name:"无尘",Age:18,Address:Address{City:"北京"}} //map赋值
p := Person{"无尘",18,Address{"北京"}}
// 获取目标对象
t := reflect.TypeOf(p)
fmt.Println("t:", t)
// .Name()可以获取去这个类型的名称
fmt.Println("类型的名称:", t.Name())
// 获取目标对象的值类型
v := reflect.ValueOf(p)
fmt.Println("v:", v)
// .NumField()获取其包含的字段的总数
for i := 0; i < t.NumField(); i++ {
// 从0开始获取Person所包含的key
key := t.Field(i)
// interface方法来获取key所对应的值
value := v.Field(i).Interface()
fmt.Printf("第%d个字段是:%s:%v = %v \n", i+1, key.Name, key.Type, value)
}
// 取出这个City的详情打印出来
fmt.Printf("%#v\n", t.FieldByIndex([]int{2, 0}))
// .NumMethod()来获取Person里的方法
for i:=0;i<t.NumMethod(); i++ {
m := t.Method(i)
fmt.Printf("第%d个方法是:%s:%v\n", i+1, m.Name, m.Type)
}
}
运行结果:
t: main.Person
类型的名称: Person
v: {无尘 18 {北京}}
第1个字段是:Name:string = 无尘
第2个字段是:Age:uint = 18
第3个字段是:Address:main.Address = {北京}
reflect.StructField{Name:"City", PkgPath:"", Type:(*reflect.rtype)(0x4cfe60), Tag:"", Offset:0x0, Index:[]int{0}, Anonymous:false}
第1个方法是:Hello:func(main.Person)
- 通过反射修改内容
package main
import (
"reflect"
"fmt"
)
type Person struct {
Name string
Age int
}
func main() {
p := &Person{"无尘",18}
v := reflect.ValueOf(p)
// 修改值必须是指针类型
if v.Kind() != reflect.Ptr {
fmt.Println("非指针类型,不能进行修改")
return
}
// 获取指针所指向的元素
v = v.Elem()
// 获取目标key的Value的封装
name := v.FieldByName("Name")
if name.Kind() == reflect.String {
name.SetString("wucs")
}
fmt.Printf("%#v \n", *p)
// 如果是整型的话
test := 666
testV := reflect.ValueOf(&test)
testV.Elem().SetInt(999)
fmt.Println(test)
}
运行结果:
main.Person{Name:"wucs", Age:18}
999
- 通过反射调用方法
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func (p Person) EchoName(name string){
fmt.Println("我的名字是:", name)
}
func main() {
p := Person{Name: "无尘",Age: 18}
v := reflect.ValueOf(p)
// 获取方法控制权
// 官方解释:返回v的名为name的方法的已绑定(到v的持有值的)状态的函数形式的Value封装
mv := v.MethodByName("EchoName")
// 拼凑参数
args := []reflect.Value{reflect.ValueOf("wucs")}
// 调用函数
mv.Call(args)
}
运行结果:
我的名字是: wucs
第二部分:Go 的高效并发编程实例
- 本次给大家介绍的是go编程基础,下一节的并发编程后续会推出。感谢大家的观看。
- 欢迎留言交流,指正
- 我的微信 wucs_dd ,公众号 《微客鸟窝》,专注于go开发技术分享。
标签:函数,必看,int,fmt,类型,入门者,func,Go,string 来源: https://blog.csdn.net/qq_23003093/article/details/118614873