写点什么

一脚踢你进 Go 语言大门!入门者必看,万字长文,建议收藏!

用户头像
微客鸟窝
关注
发布于: 7 小时前
一脚踢你进Go语言大门!入门者必看,万字长文,建议收藏!

@[toc]


您诸位好啊,我是无尘!

第一部分:一脚踢你进 Go 语言大门!

Ⅰ、基础不牢,地动山摇

1.第一个例子:Hello World

package mainimport "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 目录下,以供终端使用。


  1. 若工作目录为 /Users/wucs/go,需要把 GOPATH 环境变量设置为 /Users/wucs/go,把 GOBIN 环境变量设置为 $GOPATH/bin

  2. Linux/macOS 下,把以下内容添加到 /etc/profile 或者 $HOME/.profile 文件保存即可:


   export GOPATH=/Users/wucs/go   export GOBIN=$GOPATH/bin
复制代码

3.项目结构

我们采用 Go Module 模式开进行开发,此模式不必将代码放在 GOPATH 目录中,可以在任意位置来创建项目。


  1. 比如项目位置为 \golang\gotour,打开终端,切换到项目目录,然后执行go mod init example.com/hello,会生成一个 go.mod 文件。然后在项目根目录创建 main.go 文件。

  2. go mod 是 Golang 1.11 版本引入的官方包(package)依赖管理工具,用于解决之前没有地方记录依赖包具体版本的问题,方便依赖包的管理。

    • go mod init “module名字”初始化模块。

    • go mod tidy 增加缺失的包,移除没用的包

  3. 将文章开始的 Hello World 实例写入到 main.go 文件中。

  4. main.go 就是整个项目的入口文件,里面有 mian 函数。

4.编译发布

  1. 在项目根目录执行 go build ./main.go,会在项目根目录生成 main.exe 文件

  2. 在项目根目录下,终端输入 main 回车,成功打印 “Hello World”,说明程序成功运行。

  3. 以上生成的可执行文件在项目根目录,也可以把它安装到 $GOBIN 目录或者其他任意位置:

  4. go install /main.go

  5. 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 更多的组合,参考官方文档的 GOARCH 这一节。

Ⅱ、数据类型

1. 都有哪些类型

变量声明

  1. var 变量名 类型 = 表达式

  2. var i int = 10

  3. 类型推导

  4. var i = 10

  5. 可以根据值的类型来省略变量类型

  6. 声明多个变量


 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


注意:

  1. 有符号整型可以表示负数、零、正数,而无符号整型只能为零和正数。

  2. int 和 uint 这两个没有具体的 bit 大小的整型,他们大小可能是 32bit,也可能是 64bit,这个取决于硬件设备 CPU。

  3. 在整型中,如果能确定 int 的 bit 就使用明确的 int 类型,这一有助于程序的移植性。

  4. 还有一种字节类型 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:=&ifmt.Println(*pi)
复制代码

4.常量

常量的值是在编译期就确定好的,确定后不能被修改,可以防止在运行期被恶意篡改。

常量定义

和变量类型,只不过使用关键字 const


const name = "无尘"


在 Go 语言中,只允许布尔型、字符串、数字类型这些基础类型作为常量。

5.iota

iota 是一个常量生成器,可以用来初始化相似规则的常量,避免重复的初始化。


const (  one = 1  two = 2  three = 3)
//使用 iotaconst ( one = iota+1 two three)
复制代码


iota 的初始值是 0。

6.字符串

  1. 字符串和数字互换


Go 是强类型语言,不同类型的变量是不能相互使用和计算的。不同类型的变量在进行复制或计算时,需要先进行类型转换。


i := 10itos := strconv.Itoa(i)stoi,err := strconv.Atoi(itos)fmt.Println(itos,stoi,err) //10 10 nil
复制代码


  1. String 包


string 包是 Go SDK 提供的一个标准包。用于处理字符串的工具包。包含查找字符串、拆分字符串、去除字符串的空格、判断字符串是否含有某个前缀或后缀。


//判断s1的前缀是否是Hfmt.Println(strings.HasPrefix(s1,"H"))//在s1中查找字符串ofmt.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")    }}
复制代码


注意:

  1. if 后的表达无 ‘( )’

  2. 每个条件分支中的 ‘{ }’ 是必须的。哪怕只有一行代码。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 := 0for i := 1; i <= 100; i++{  sum += i}fmt.Println("sum:",sum)
复制代码


第一部分是简单语句第二部分是 for 循环的条件第三部分是更新语句这三部分组成都不是必须的,可以被省略。


Go 语言中没有 while 循环,可以通过 for 达到 while 的效果:


sum := 0i := 1for i <= 100 {  sum += 1  i++}
复制代码


Go 中,同样支持 continue,break 控制 for 循环。


  1. continue 跳出本次循环,进入下次循环。

  2. break 强行退出整个循环。

Ⅳ、集合类型

1. Array(数组)

数组存放的是固定长度、相同类型的数据。

1.1 数组声明

  1. var <数组名> = [<长度>]<元素>{元素 1,元素 2}

  2. var arr = [2]int{1,2}

  3. 或者

  4. arr := [2]int{1,2}

  5. var <数组名> = [...]<元素类型>{元素 1,元素 2}

  6. var arr = [...]int{1,2}

  7. 或者

  8. arr := [...]int{1,2}

  9. var <数组名> = [...]<类型>{索引 1:元素 1,索引 2:元素 2}

  10. var arr = [...]int{1:1,0:2}

  11. 或者

  12. arr := [...]int{1:1,0:2}


数组的每个元素在内存中都是连续存放的,每个元素都有一个下标,下标从 0 开始。

数组长度可以省略,会自动根据{}中的元素来进行推导。

没有初始化的索引,默认值是数组类型的零值。

1.2 数组循环

for i,v := range array {  fmt.Printf("索引:%d,值:%s\n",i,v)}
复制代码


  1. range 表达式返回数组索引赋值给 i,返回数组值赋值给 v。

  2. 如果返回的值用不到,可以用 _ 下划线丢弃:


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的切片,长度是4slice := make([]string,4)//长度是4,容量是8slice1 := 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 声明初始化

  1. make:

  2. mapName := make(map[string]int)

  3. 字面量:

  4. mapName := map[string]int{"无尘":29}


如果不想创建的时候添加键值对,使用空大括号{}即可,切记不能省略。

3.2 Map 获取、删除

//添加键值对或更新对应的key的valuemapName["无尘"] = 20//获取指定key的valueage := mapName["无尘"]
复制代码


获取不存在的 k-v 键值对时,如果 key 不存在,返回的 value 是该值的零值,所以很多时候,需要先判断 map 中的 key 是否存在。


nameAge := make([string]int)nameAge["无尘"]=29age,ok := nameAge["无尘"]if ok {  fmt.Println(age)}
复制代码


  • map 的 [] 操作返回两个值

  • 第一个是 value

  • 第二个是标记该 key 是否存在,存在则为 true


delete()函数进行删除


delete(nameAge,"无尘")


  • delete 有两个参数,一个是 map,一个是要删除的 key 。

4. 遍历 Map

nameAge["无尘"] = 29nameAge["无尘1"] = 30nameAge["无尘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 }}
//结果为:123
复制代码


由于闭包函数,sum 函数返回一个匿名函数,匿名函数持有外部函数 sum 的变量 i,所以在 main 函数中,每次调用 sum(),i 的值就会 +1。


在 Go 语言中,函数也是一种类型,可以作为函数类型的变量、参数、或者一个函数的返回值。

2. 方法

方法和函数类似,不同之处就是方法必须有一个接收者,这个接收者是一个“类”(类型),这样这个方法就算属于这个“类”。


type Name stringfunc (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 //人的年龄}
复制代码


  1. type 与 struct 是关键字,用来定义一个新结构体的类型。

  2. person 为结构体名字。

  3. name/age 为结构体的字段名,后面指对应的字段类型。


  • 字段声明和变量类似,变量名在前,类型在后

  • 字段可以是人一个,一个字段都没有的结构体,成为空结构体。

  • 结构体也是一种类型,比如 person 结构体和 person 类型是一个意思。

1.2 声明

  1. 像普通字符串、整型医院声明初始化var p person


声明了一个 person 类型的变量 p,但是没有初始化,所以默认使用结构体里字段的零值。


  1. 字面量方式初始化p := person{"无尘",18}


表示结构体变量 p 的 name 字段初始化为“无尘”,age 字段初始化为 18。顺序必须和字段定义顺序一致。


  1. 根据字段名称初始化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()stringtype 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. 值接受者、指针接受者

  1. 实现一个接口,必须实现接口中所有的方法。

  2. 定义一个方法,有值类型接收者和指针类型接收者,两者都可以调用方法,因为 Go 编译器自动做了转换。

  3. 但是接口的实现,值类型接收者和指针类型接收者不一样


上面接口体 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),代码编译不通过,表明以指针类型接收者实现接口,只有对应的指针类型才被认为实现了接口



  • 当值类型作为接收者,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 mainimport "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:


  1. func TypeOf(i interface{}) Type

  2. func ValueOf(i interface{}) Value



示例:


package mainimport (  "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: 微客鸟窝}
复制代码


  1. 函数 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}
复制代码


  1. 函数 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类型的名称: Personv: {无尘 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)
复制代码


  1. 通过反射修改内容


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
复制代码


  1. 通过反射调用方法


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 编程基础,下一节的并发编程后续会推出。感谢大家的观看。

  • 欢迎留言交流,指正

  • 我的 wx: wucs_dd ,公号 《微客鸟窝》,专注于 go 开发技术分享。


发布于: 7 小时前阅读数: 5
用户头像

微客鸟窝

关注

还未添加个人签名 2019.11.01 加入

公众号《微客鸟窝》笔者,目前从事web后端开发,涉及语言PHP、golang。

评论

发布
暂无评论
一脚踢你进Go语言大门!入门者必看,万字长文,建议收藏!