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

Posted 微客鸟窝

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一脚踢你进Go语言大门!入门者必看,万字长文,建议收藏!相关的知识,希望对你有一定的参考价值。

@[toc]

您诸位好啊,我是无尘!

第一部分:一脚踢你进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 目录下,以供终端使用。
  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 文件。

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

    • go mod init “module名字”初始化模块。
    • go mod tidy 增加缺失的包,移除没用的包
  1. 将文章开始的 Hello World 实例写入到 main.go 文件中。

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

4.编译发布

  1. 在项目根目录执行 go build ./main.go,会在项目根目录生成 main.exe 文件
  2. 在项目根目录下,终端输入 main 回车,成功打印 “Hello World”,说明程序成功运行。
  3. 以上生成的可执行文件在项目根目录,也可以把它安装到 $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. 都有哪些类型

变量声明

  1. var 变量名 类型 = 表达式
    var i int = 10
  2. 类型推导
    var i = 10
    可以根据值的类型来省略变量类型
  3. 声明多个变量

    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:=&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.字符串

  1. 字符串和数字互换

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

i := 10
itos := strconv.Itoa(i)
stoi,err := strconv.Atoi(itos)
fmt.Println(itos,stoi,err) //10 10 nil
  1. 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")
    }
}

注意:

  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 := 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循环。

  1. continue 跳出本次循环,进入下次循环。
  2. break 强行退出整个循环。

Ⅳ、集合类型

1. Array(数组)

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

1.1数组声明

  1. var <数组名> = [<长度>]<元素>{元素1,元素2}
    var arr = [2]int{1,2}
    或者
    arr := [2]int{1,2}
  2. var <数组名> = [...]<元素类型>{元素1,元素2}
    var arr = [...]int{1,2}
    或者
    arr := [...]int{1,2}
  3. 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)
}
  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的切片,长度是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 声明初始化

  1. make:
    mapName := make(map[string]int)
  2. 字面量:
    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 //人的年龄
}
  1. type 与 struct 是关键字,用来定义一个新结构体的类型。
  2. person 为结构体名字。
  3. name/age 为结构体的字段名,后面指对应的字段类型。
  • 字段声明和变量类似,变量名在前,类型在后
  • 字段可以是人一个,一个字段都没有的结构体,成为空结构体。
  • 结构体也是一种类型,比如 person 结构体和 person 类型是一个意思。

1.2 声明

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

    声明了一个person类型的变量p,但是没有初始化,所以默认使用结构体里字段的零值。
  2. 字面量方式初始化
    p := person{"无尘",18}

    表示结构体变量 p 的name字段初始化为“无尘”,age字段初始化为18。顺序必须和字段定义顺序一致。
  3. 根据字段名称初始化
    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. 值接受者、指针接受者

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

方法接收者实现接口类型
(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:

  1. func TypeOf(i interface{}) Type
  2. 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:  微客鸟窝
}
  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
    }
  2. 函数 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)
  3. 通过反射修改内容

    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
  4. 通过反射调用方法

    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开发技术分享。

以上是关于一脚踢你进Go语言大门!入门者必看,万字长文,建议收藏!的主要内容,如果未能解决你的问题,请参考以下文章

万字长文!入门Docker,这一篇就够了

Vue学习之从入门到神经(万字长文 建议收藏)

Python数据可视化实战应用万字长文从入门到高端(建议收藏)

万字长文玩转Spark面试: 进大厂必看!

万字长文 Vue全家桶从入门到实战,超详细笔记整理 ( 三 ) (建议收藏)

万字长文 Vue全家桶从入门到实战,超详细笔记整理 ( 一 ) (建议收藏)