type关键字

type:用于类型定义和类型别名

  • 类型定义:type 类型名 Type
  • 类型别名:type 类型名 = Type

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
"fmt"
"strconv"
)

func main() {
var i1 myint
var i2 = 100 //int
i1 = 200
fmt.Println(i1, i2)

var name mystr
name = "王二狗"
var s1 string
s1 = "李小花"
fmt.Println(name, s1)

//i1 = i2 //cannot use i2 (type int) as type myint in assignment

//name = s1 //cannot use s1 (type string) as type mystr in assignment

fmt.Printf("%T,%T,%T,%T\n", i1, i2, name, s1) //main.myint,int,main.mystr,string

fmt.Println("----------------------------------")
res1 := fun1()
fmt.Println(res1(10, 20))

fmt.Println("----------------------------------")
var i3 myint2
i3 = 1000
fmt.Println(i3)
i3 = i2
fmt.Println(i3)
fmt.Printf("%T,%T,%T\n", i1, i2, i3) //main.myint,int,int

}

// 1.定义一个新的类型
type myint int
type mystr string

// 2.定义函数类型
type myfun func(int, int) string

func fun1() myfun { //fun1()函数的返回值是myfun类型
fun := func(a, b int) string {
s := strconv.Itoa(a) + strconv.Itoa(b)
return s
}
return fun
}

// 3.类型别名
type myint2 = int //不是重新定义新的数据类型,只是给int起别名,和int可以通用

输出结果:

1
2
3
4
5
6
7
8
9
200 100
王二狗 李小花
main.myint,int,main.mystr,string
----------------------------------
1020
----------------------------------
1000
100
main.myint,int,int

尽管类型别名可以方便地使用现有类型,但它们不会创建一个新的类型。因此,无法在类型别名上定义新方法。只有在本地类型上才能定义新方法。

错误代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "time"

func main() {

}

type MyDuration = time.Duration

func (m MyDuration) SimpleSet() { //cannot define new methods on non-local type time.Duration

}

要么在time包下定义新方法,要么建一个新的命名类型,而不是使用类型别名。

修正:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "time"

type MyDuration time.Duration

func (m MyDuration) SimpleSet() {
// 在这里实现SimpleSet方法
}

func main() {
// 在这里使用MyDuration类型
}

在结构体成员嵌入时使用别名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import "fmt"

type Person struct {
name string
}

func (p Person) show() {
fmt.Println("Person--->", p.name)
}

// People 类型别名
type People = Person

func (p People) show2() {
fmt.Println("People--->", p.name)
}

type Student struct {
//嵌入两个结构体
Person
People
}

func main() {
var s Student
//s.name = "王二狗" //ambiguous selector s.name
s.Person.name = "王二狗"
//s.show() //ambiguous selector s.show
s.Person.show()

fmt.Printf("%T,%T\n", s.Person, s.People) //main.Person,main.Person

s.People.name = "李小花"
s.People.show2()

s.Person.show()
}

输出结果:

1
2
3
4
Person---> 王二狗
main.Person,main.Person
People---> 李小花
Person---> 王二狗

错误处理

在实际工程项目中,我们希望通过程序的错误信息快速定位问题,但是又不喜欢错误处理代码写的冗余而又啰嗦。Go语言没有提供像Java、c#语言中的try…catch异常处理方式,而是通过函数返回值逐层往上抛。这种设计,鼓励工程师在代码中显式的检查错误,而非忽略错误,好处就是避免漏掉本应处理的错误。但是带来一个弊端,让代码啰嗦。

什么是错误

错误是什么?
错误指的是可能出现问题的地方出现了问题。比如打开一个文件时失败,这种情况在人们的意料之中。
而异常指的是不应该出现问题的地方出现了问题。比如引用了空指针,这种情况在人们的意料之外。可见,错误是业务过程的一部分,而异常不是。
Go中的错误也是一种类型。错误用内置的error类型表示。就像其他类型的,如int,float64。错误值可以存储在变量中,从函数中返回,等等。

演示错误

让我们从一个示例程序开始,这个程序尝试打开一个不存在的文件。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"os"
)

func main() {
f, err := os.Open("test.txt")
if err != nil {
//log.Fatal(err)
fmt.Println(err) //open test.txt: no such file or directory
if ins, ok := err.(*os.PathError); ok {
fmt.Println("1.Op:", ins.Op)
fmt.Println("2.Path:", ins.Path)
fmt.Println("3.Err:", ins.Err)
}
return
}
fmt.Println(f.Name(), "打开文件成功。。")

}

输出结果:

1
2
3
4
open test.txt: The system cannot find the file specified.
1.Op: open
2.Path: test.txt
3.Err: The system cannot find the file specified.

这行代码尝试将错误值 err 转换为 *os.PathError 类型,并将结果存储在变量 ins 中。这是一个类型断言的示例,它检查错误类型是否是 *os.PathError,并且返回一个布尔值 ok 表示是否成功进行了类型转换。如果类型断言成功,这些代码将打印 *os.PathError 类型的特定字段信息。Op 字段表示操作类型,Path 字段表示操作的路径,Err 字段表示底层错误。

自定义函数返回错误

error:内置的数据类型,内置的接口
定义方法:Error() string

使用go语言提供好的包:
errors包下的函数:New(),创建一个error对象,fmt包下的Errorf()函数:
func Errorf(format string, a …interface{}) error

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"errors"
"fmt"
)

func main() {
//1.创建一个error数据
err1 := errors.New("自己创建玩的。。")
fmt.Println(err1)
fmt.Printf("%T\n", err1) //*errors.errorString

//2.另一个创建error的方法
err2 := fmt.Errorf("错误的信息码:%d", 100)
fmt.Println(err2)
fmt.Printf("%T\n", err2)

fmt.Println("-----------------")
err3 := checkAge(-30)
if err3 != nil {
fmt.Println(err3)
return
}
fmt.Println("程序。。。go on。。。")
}

// 设计一个函数:验证年龄是否合法,如果为负数,就返回一个error
func checkAge(age int) error {
if age < 0 {
//返回error对象
//return errors.New("年龄不合法")
err := fmt.Errorf("您给定的年龄是:%d,不合法", age)
return err
}
fmt.Println("年龄是:", age)
return nil
}

输出结果:

1
2
3
4
5
6
自己创建玩的。。
*errors.errorString
错误的信息码:100
*errors.errorString
-----------------
您给定的年龄是:-30,不合法

错误的类型表示

1
2
3
4
5
if ins, ok := err.(*os.PathError); ok {
fmt.Println("1.Op:", ins.Op)
fmt.Println("2.Path:", ins.Path)
fmt.Println("3.Err:", ins.Err)
}
  1. 获得更多信息的第二种方法是断言底层类型,并通过调用struct类型的方法获取更多信息。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"net"
)

func main() {
addr, err := net.LookupHost("www.baidu.com")
fmt.Println(err)
if ins, ok := err.(*net.DNSError); ok {
if ins.Timeout() {
fmt.Println("操作超时。。")
} else if ins.Temporary() {
fmt.Println("临时性错误。。")
} else {
fmt.Println("通常错误。。")
}
}
fmt.Println(addr)
}

输出结果:

1
2
<nil>
[182.61.200.6 182.61.200.7]

3.直接比较
获得更多关于错误的详细信息的第三种方法是直接与类型错误的变量进行比较。让我们通过一个例子来理解这个问题。
filepath包的Glob函数用于返回与模式匹配的所有文件的名称。当模式出现错误时,该函数将返回一个错误ErrBadPattern。
在filepath包中定义了ErrBadPattern,如下所述:

1
var ErrBadPattern = errors.New("syntax error in pattern")

errors.New()用于创建新的错误。
当模式出现错误时,由Glob函数返回ErrBadPattern。

让我们写一个小程序来检查这个错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"path/filepath"
)

func main() {
files, err := filepath.Glob("[")
if err != nil && err == filepath.ErrBadPattern {
fmt.Println(err) //syntax error in pattern
return
}
fmt.Println("files:", files)
}

输出结果:

1
syntax error in pattern

记住永远不要忽略一个错误。忽视错误会招致麻烦。

自定义错误

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"math"
)

func main() {
radius := -3.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
if err, ok := err.(*areaError); ok {
fmt.Printf("半径是:%.2f\n", err.radius)
}
return
}
fmt.Println("圆形的面积是:", area)

}

// 1.定义一个结构体,表示错误的类型
type areaError struct {
msg string
radius float64
}

// 2.实现error接口,就是实现Error()方法
func (e *areaError) Error() string {
return fmt.Sprintf("error:半径,%.2f,%s", e.radius, e.msg)
}

func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, &areaError{"半径是非法的", radius}
}
return math.Pi * radius * radius, nil
}

输出结果:

1
2
error:半径,-3.00,半径是非法的
半径是:-3.00

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import "fmt"

func main() {
length, width := -6.7, -9.1
area, err := rectArea(length, width)
if err != nil {
fmt.Println(err)
if err, ok := err.(*areaError); ok {
if err.legnthNegative() {
fmt.Printf("error:长度,%.2f,小于零\n", err.length)
}
if err.widthNegative() {
fmt.Printf("error:宽度,%.2f,小于零\n", err.width)
}
}
return
}
fmt.Println("矩形的面积是:", area)
}

type areaError struct {
msg string //错误的描述
length float64 //发生错误的时候,矩形的长度
width float64 //发生错误的时候,矩形的宽度
}

func (e *areaError) Error() string {
return e.msg
}

func (e *areaError) legnthNegative() bool {
return e.length < 0
}

func (e *areaError) widthNegative() bool {
return e.width < 0
}

func rectArea(length, width float64) (float64, error) {
msg := ""
if length < 0 {
msg = "长度小于零"
}
if width < 0 {
if msg == "" {
msg = "宽度小于零"
} else {
msg += ",宽度也小于零"
}
}

if msg != "" {
return 0, &areaError{msg, length, width}
}
return length * width, nil
}

输出结果:

1
2
3
长度小于零,宽度也小于零
error:长度,-6.70,小于零
error:宽度,-9.10,小于零

panic和recover

Golang中引入两个内置函数panic和recover来触发和终止异常处理流程,同时引入关键字defer来延迟执行defer后面的函数。
一直等到包含defer语句的函数执行完毕时,延迟函数(defer后的函数)才会被执行,而不管包含defer语句的函数是通过return的正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。
当程序运行时,如果遇到引用空指针、下标越界或显式调用panic函数等情况,则先触发panic函数的执行,然后调用延迟函数。调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数等。如果一路在延迟函数中没有recover函数的调用,则会到达该协程的起点,该协程结束,然后终止其他所有协程,包括主协程(类似于C语言中的主线程,该协程ID为1)。
panic:

  1. 内建函数

  2. 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行

  3. 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行,这里的defer有点类似 try-catch-finally中的finally

  4. 直到goroutine整个退出,并报告错误

recover:

  1. 内建函数
  2. 用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
  3. 一般的调用建议
    a).在defer函数中,通过recover来终止一个gojroutine的panicking过程,从而恢复正常代码的执行
    b).可以获取通过panic传递的error

简单来讲: go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import "fmt"

func main() {
defer func() {
if msg := recover(); msg != nil {
fmt.Println(msg, "程序恢复啦。。。")
}
}()
funA()
defer myprint("defer main:3.....")
funB()
defer myprint("defer main:4.....")

fmt.Println("main..over。。。。")

}
func myprint(s string) {
fmt.Println(s)
}

func funA() {
fmt.Println("我是一个函数funA()....")
}

func funB() { //外围函数

fmt.Println("我是函数funB()...")
defer myprint("defer funB():1.....")

for i := 1; i <= 10; i++ {
fmt.Println("i:", i)
if i == 5 {
//让程序中断
panic("funB函数,恐慌了")
}
} //当外围函数的代码中发生了运行恐慌,只有其中所有的已经defer的函数全部都执行完毕后,该运行恐慌才会真正被扩展至调用处。
defer myprint("defer funB():2.....")
}

输出结果:

1
2
3
4
5
6
7
8
9
10
我是一个函数funA()....
我是函数funB()...
i: 1
i: 2
i: 3
i: 4
i: 5
defer funB():1.....
defer main:3.....
funB函数,恐慌了 程序恢复啦。。。

由于恐慌发生在循环内部,之后的语句将不会执行。然而,之前已经被defer关键字延迟执行的myprint函数仍然会执行。因此,”defer funB():1…..”会被输出。funB函数的执行完毕后,恐慌会被传播到调用处,也就是main函数中的匿名函数。在这个匿名函数中,我们通过recover函数检测到了恐慌,所以会输出恐慌信息:”funB函数,恐慌了”,并打印”程序恢复啦。。。”。

错误和异常从Golang机制上讲,就是error和panic的区别。很多其他语言也一样,
比C++/Java,没有error有errno,没有panic但有throw。
Golang错误和异常是可以互相转换的:
1.错误转异常,比如程序逻辑上尝试请求某个URL,最多尝试三次,尝试三次的过程中请求失败是错误,尝试完第三次还不成功的话,失败就被提升为异常了。
2.异常转错误,比如panic触发的异常被recover恢复后,将返回值中error类型的变量进行赋值,以便上层函数继续走错误处理流程。

什么情况下用错误表达,什么情况下用异常表达,就得有一套规则,否则很容易出现一切皆错误或一切皆异常的情况。
以下给出异常处理的作用域(场景)∶

1.空指针引用
2.下标越界
3.除数为0
4.不应该出现的分支,比如default
5.输入不应该引起函数错误

其他场景我们使用错误处理,这使得我们的函数接口很精炼。对于异常,我们可以选择在一个合适的上游去recover,并打印堆栈信息,使得部署后的程序不会终止。