Jochen的golang小抄-基础篇-章三

本系列写的是学习过程中的纯代码笔记记录,该系列为代码流,基本只写代码,代码开始前会有一段导读注释,建议先看注释在学习和练习代码

小抄系列主要分为皮毛、基础和进阶篇,本篇为基础,篇幅较长,故分为多个章节,本章主要讲golang中的方法和接口的知识

方法

  • 方法和我们前面一直使用函数算是近亲,方法是一个函数
  • 它带有一个特殊的接收器类型(或者叫接收者,它必须是一个结构体或者结构体指针),该类型在func和方法名之间定义
  • 调用时,接收者可以是该结构体的一个值或指针,接收器可以在函数内部被访问

简单说,可以认为,方法就是函数,只不过要指定调用者

方法和使用和函数也一模一样,只不过多了接收者,即谁来调用

牛刀小试

package main

import "fmt"

func main() {
	/*
		方法(method):
			一个方法就是一个包含了接收者的函数,所有给定类型的方法属于该类型的方法集

		语法:
			func (接收器) 方法名(参数列表)(返回值列表){

			}

		与函数的区别:
			1.需要有调用者而已
			2.可以定义多个同名方法(接收器类型不同)
	*/

	p := Person{name: "jochen", age: 18, sex: "boy"}
	//调用方法
	p.run()
	/*
		wc?有没有一种醍醐灌顶的感觉,这不就是类的行为,对象的动作
		对于学习过.net的小伙伴应该更熟悉了,这不是扩展方法吗?甚至更过强大且定义更加合理
	*/

	//调用时,接收者可以是该结构体的一个值或指针,接收器可以在函数内部被访问
	p2 := &p
	fmt.Printf("%T\n", p2) //*main.Person 指针类型
	p2.run()               //该结构体类型的指针也可以访问该方法

	p.talk()  //结构体可以调用接收器为指针类型的方法
	p2.talk() //指针类型调用接收器是指针类型的方法

	/*
		方法还有一点与函数不同的是,方法可以定义多个同名方法,只要其接收器类型不一样即可(是不是想起方法的重载)
		而函数在包下只能定义一个同名方法,不存在其他语言中参数列表不同构成重载的情况
	*/
	//不同的对象在调用时,执行的是不同的方法
	b := Bird{name: "鹦鹉"}
	b1 := &b
	b.talk()
	b.run()
	b1.talk()
	b1.run()
}

//1.定义一个结构体
type Person struct {
	name string //字段
	age  int
	sex  string
}

type Bird struct {
	name string
}

//2.定义行为方法
func (p Person) run() { //这里p就和传递了参数进来一样,只不过限制了这个函数只能被通过Person类型去调用
	fmt.Println(p.name, "在跑...") //方法的接收者可以在方法的内部访问

}

func (p *Person) talk() { //接收器可以是结构体指针
	fmt.Println(p.name, "在balabalala...")
}

//可以定义多个同名方法
func (b Bird) run() { //这里p就和传递了参数进来一样,只不过限制了这个函数只能被通过Person类型去调用
	fmt.Println(b.name, "在跑...") //方法的接收者可以在方法的内部访问

}

func (b *Bird) talk() { //接收器可以是结构体指针
	fmt.Println(b.name, "在balabalala...")
}

go语言因为不是纯面向对象的语言,其不支持class类,所以提供方法可以理解为为了实现类似与类的行为的方式

方法对比函数的意义:

  • 方法:是某个类别的行为功能,需要指定接收者调用
  • 函数:一段独立功能的代码,可以直接调用

“继承”中的方法

前面已经通过嵌套结构体模拟了go语言中模拟继承特性的实现,方法也是可以继承的,如果匿名字段实现了一个方法没那么包含这个匿名字段的结构体也能调用该方法

package main

import "fmt"

func main() {
	/*
		go语言虽然不是面向对象的,也谈不上继承性,但是在一开始时,有介绍过go通过嵌套的匿名结构体
			模拟oop的继承性的方式

		OOP中的继承性:两个class存在继承关系,一个为子类,另一个为父类,那么:
			1.子类可以直接访问父类的属性和方法(在go中的体现为字段提升)
			2.子类可以派生自己的属性和方法(在go中体现在结构体嵌套中)
			3.子类可以重写父类的方法(override,即将父类已有的方法推翻重写实现)
				(直接定义子类接收器类型的父类同名方法)

	*/
	//模拟oop中的继承性
	//创建Person类型
	p1 := Person{name: "Jochen", age: 18}
	fmt.Println(p1.name, p1.age) //父类对象,访问父类的字段属性
	p1.sleep()                   //父类对象,访问父类的方法

	//创建子类对象
	s1 := Student{Person{name: "小明", age: 12}, "三年二班"}
	fmt.Println(s1.name, s1.age) //子类对象,访问父类的字段属性(提升字段)
	fmt.Println(s1.class)        //子类对象,子类访问新增的字段属性
	s1.sleep()                   //子类对象,访问父类的方法
	s1.read()                    //子类对象,子类访问新增的方法
	//定义了子类接收器同名方法后,相当于重写父类的方法
	s1.sleep() //如果存在方法的重写,子类访问重写的方法

}

//1.定义一个"父类"
type Person struct {
	name string
	age  int
}

//2.定义一个"子类"
type Student struct {
	Person        //匿名成员 结构体嵌套,模拟类的继承性
	class  string //班级  子类新增字段
}

//3.提供Person方法
func (p Person) sleep() {
	fmt.Println("父类的方法", p.name, "在呼噜呼噜~!")
}

func (s Student) read() {
	fmt.Println("子类新增的方法", "学生", s.name, "在学习!")
}

//3.提供Person方法
func (p Student) sleep() {
	fmt.Println("子类重写的方法", "学生:", p.name, "在呼噜呼噜~!")
}

接口

接口定义了对象的行为, 接口可以理解为是一种行为的规范或者说是规则,它规范了类型必须要可以做什么(一组行为或方法),但是具体细节由对象自己去实现

接口的本质是把功能的定义和功能的实现分离开,从而降低程序的依赖性和耦合度

在Go中,接口指定了类型应该具有的方法(只是声明方法,不包含具体实现),具体方法的实现由类型自己去决定。当类型为接口中的所有方法提供实现时,就称为实现接口

简单说,接口把一些具有共性的方法定义在一块,但不提供具体的代码实现,任何类型只要实现了这些方法就是实现了接口

接口的使用

package main

import "fmt"

func main() {
	/*
		接口(interface):
			go中,接口就是一组方法的定义

		go中,接口和类型的实现不是侵入式的
			即不需要像别的语言一样去声明哪个类实现了什么接口,例如:class xx:interfaceName
			更何况go中没有类的概念,都是通过结构体去实现的

		接口的使用:
			1.当接口作为函数参数的时候,可以传入任何该接口的实现类
			2.接口对象不能访问实现类中的属性
			
		多态:一个事物的多种形态
			go通过接口模拟多态
				- 接口的实现类既可以看作是本身的类型,此时只能访问类中的属性和方法
				- 也可以看作是接口类型,此时只能访问接口中的方法
	*/

	//4.当接口作为函数参数的时候,可以传入任何该接口的实现类
	//创建鼠标和键盘对象
	m := Mouse{name: "雷蛇"}
	k := Keyboard{"cherry"}
	//不同的类型调用同一个方法,实现的逻辑不同,这就是多态性
	testUsbInterface(m) //鼠标实现了接口,所以可以作为参数传入
	testUsbInterface(k) //键盘实现了接口,所以可以作为参数传入
	/*
		print:
			雷蛇 鼠标已成功接入,可以开始你的操作了!
			雷蛇 鼠标被拔掉了,骚不动了吧!
			cherry 开敲吧,键盘侠
			cherry 没得敲了,老键
	*/

	//定义接口对象
	var usb Usb = m
	usb.start()
	usb.end()
	//usb.name  //usb.name undefined (type Usb has no field or method name)  接口对象不能访问实现类中的属性
}

//1.定义接口
type Usb interface {
	//定义方法:方法名([参数列表])[返回值列表]  	ps:[]表示可选项
	start() //usb设备开始工作
	end()   //usb设备结束工作

}

//2.实现类 鼠标
type Mouse struct {
	name string
}

//实现类
type Keyboard struct {
	name string
}

//3.实现接口 在goland ide中使用ctrl+u(windows或linux中,mac为command+u)可以看到实现的是哪个接口,并跳转
func (m Mouse) start() { //只有实现了接口里面的所有方法,这个对象才算是实现了接口
	fmt.Println(m.name, "鼠标已成功接入,可以开始你的操作了!")
}

func (m Mouse) end() {
	fmt.Println(m.name, "鼠标被拔掉了,骚不动了吧!")
}

func (u Keyboard) start() {
	fmt.Println(u.name, "开敲吧,键盘侠")
}
func (u Keyboard) end() {
	fmt.Println(u.name, "没得敲了,老键")
}


//测试方法 其参数为接口类型
func testUsbInterface(usb Usb)  { //使用接口作为参数,表示一种规范,满足我的规范的类型都可以传过来
	// 满足接口规范就实现了接口中所有方法的类型
	usb.start()
	usb.end()
}

上面案例可以看到,go语言虽然没有类的存在,但是可以通过接口实现oop的多态性

总结一下接口的主要用法:

  1. 将接口作为函数的参数,则该参数实际上可以传递实现了该接口的任意类型
  2. 定义一个类型为接口类型,该接口类型可以被赋值为任意的实现类对象
  3. 鸭子类型

拓展:鸭子类型

维基百科:在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为"鸭子"的对象,并调用它的"走"和"叫"方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的"走"和"叫"方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的"走"和"叫"方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名

鸭子类型,是一种类型推断策略,更关注对象如何被使用,而不是对象的类型本身。go的接口实现是非侵入式的,即你不用显示去让结构去实现某一个接口,只需要去实现接口中所有的方法,编译器就认为你实现了某一个接口。go语言作为一门静态语言,引入了动态语言的便利,又会进行静态语言的类型检查,写起来真的美滋滋

空接口

package main

import "fmt"

func main() {
	/*
		空接口:一个不包含方法的接口
			任意类型都是空接口实现类,也就是说使用空接口可以接收所有类型的数据
	*/
	//空接口可以接收任意类型
	var d EmptyInterface = Dog{name: "柯基"}
	var p EmptyInterface = Person{name: "小明"}
	var i EmptyInterface = 100
	var s EmptyInterface = "jochen"
	fmt.Println(d, p, i, s) //{柯基} {小明} 100 jochen

	//将接口作为函数参数可以接收任意类型的数据
	test(d)         //main.Dog, {柯基}
	test(p)         //main.Person, {小明}
	test2(888)      //int, 888
	test2("jochen") //string, jochen
	/*
		空接口作为函数最典型的例子就是fmt.Println函数
			func Println(a ...interface{}) (n int, err error)
		其接收的是一个可变长的空接口参数
	*/

	//可存储任意类型的容器,可以定义为空接口类型的容器
	//map : key字符串,value任意类型
	m := make(map[string]interface{})
	m["name"] = "Jochen"
	m["age"] = 18
	m["friend"] = Person{name: "小明"}
	fmt.Println(m) //map[age:18 friend:{小明} name:Jochen]

	//可以存储任意类型的切片
	slice1 := []interface{}{"a", 1, true, Dog{name: "二哈"}}
	fmt.Println("鱼龙混杂切片:", slice1) //鱼龙混杂切片: [a 1 true {二哈}]
	

}

//定义空接口
type EmptyInterface interface {
}

type Person struct {
	name string
}

type Dog struct {
	name string
}

//让参数为空接口类型,那么这个函数就可以接收任意类型的数据啦
func test(i EmptyInterface) {
	fmt.Printf("%T, %v\n", i, i)
}

//也可以直接写匿名空接口,不用另外定义了
func test2(i interface{}) { //匿名空接口
	fmt.Printf("%T, %v\n", i, i)
}

接口的嵌套

go语言的接口嵌套可以理解为接口的继承,即一个接口C继承了接口A和接口B,那么一个实现了接口C的类型其不仅实现了接口C中的方法,也实现了接口A和B中的方法

package main

import "fmt"

func main() {
	/*
		接口的嵌套:
			可以理解为别的语言中的接口继承
	*/
	p := Person{name: "jochen"}
	p.methodA()
	p.methodB()
	p.methonC()

	//实现类可以是接口A、B、C类型,这取决于你使用什么类型去声明或者接收
	//不同类型的接口只能调用其下定义的方法或者嵌套接口的方法
	var a A = p
	a.methodA() //只能调用接口A的方法
	var b B = p
	b.methodB() //只能调用接口B的方法
	var c C = p
	//因为c嵌套了接口A和B,可以认为继承了A和B,所以可以调用它们里面的所有方法啊
	c.methodA()
	c.methodB()
	c.methonC()

}

type A interface {
	methodA()
}

type B interface {
	methodB()
}

//嵌套接口
type C interface {
	A //嵌套接口A,可以理解为继承了接口A,实现接口C需要同时实现接口A的方法
	B //嵌套接口B,可以理解为继承了接口B,实现接口C需要同时实现接口B的方法
	methonC()
}

//实现类,该类实现了接口A、B、C
type Person struct {
	name string
}

//实现接口C
func (p Person) methodA() {
	println("实现接口A方法")
}

func (p Person) methodB() {
	println("实现接口B方法")

}

func (p Person) methonC() {
	println("实现接口C方法")
}


接口的断言

上面我们知道,通过接口我们可以实现oop中的多态性,即我们使用接口类型可以去接收任意一个实现了该接口的对象。
但是,此时通过该接口变量只能调用接口里的方法(如果是空接口,就没有方法可调用了),即使你传入的对象中有其他的方法或者想调用其中的某些成员字段,也无法去调用。
对于上述情况,可以使用接口的断言去解决,就是告诉编译器,你这个接口类型接收的是个啥玩意对象,此时就可以去调用该对象里面的所有字段和方法了

package main

import (
	"fmt"
	"math"
)

func main() {
	/*
		接口的断言:
			因为空接口没有定义任何函数,因此go语言中所有类型都实现了空接口。当一个函数的形参是空接口类型时,
		我们是无法通过该参数调用其中真实类型中的成员或者方法的,那么在函数中就需要对形参进行断言,从而得到其真实类型

		方式一:
			1.instance := 接口对象.(实际类型) //不安全,会panic()
			2.instance, ok := 接口对象.(实际类型) //安全

		方式二:
			switch instance := 接口对象.(type){
			case 实际类型1:
				...
			case 实际类型2:
				...
			...
			}
	*/
	//求三角形的面积和半径
	var t Triangle = Triangle{3, 4, 5}
	fmt.Println(t.peri())      //12
	fmt.Println(t.area())      //6
	fmt.Println(t.a, t.b, t.c) //调用三角形对象里面的字段

	//求圆的面积和半径
	var c Cricle = Cricle{2}
	fmt.Println(c.peri()) //12.566370614359172
	fmt.Println(c.area()) //12.566370614359172
	fmt.Println(c.radius) //调用圆对象里面的字段

	//定义接口类型变量 实现了该接口的类型都可以被该变量接收
	var s1 Shape
	s1 = t
	s1.area()
	s1.peri()
	//只能调用里面的实现接口的方法,想调用其中的字段就做不到了
	//s1.a //s1.a undefined (type Shape has no field or method a)
	var s2 Shape = c
	s2.area()
	s2.peri()
	//s2.radius //s2.radius undefined (type Shape has no field or method radius)
	//此时就需要通过断言去实现获取真实类型了
	getType(s1) //三角形的边长为:3.00,4.00,5.00, 周长为:12.00,面积为:6.00
	getType(s2) //圆形的半径为:2.00,周长为:12.57,面积为:12.57

	//传入的如果是地址,就是引用传递。传入的是值类型,就是值传递
	var t1 = &Triangle{1, 2, 3}
	fmt.Printf("t1: %T,%p\n", t1, t1) //t1: *main.Triangle,0xc0000c0040
	getType(t1)                       //*main.Triangle,0xc000016480

}

//通过断言去获取真实类型
func getType(s Shape) {
	//断言,其他断言方式大同小异,这里也可以使用switch,就不演示了
	if ins, ok := s.(Triangle); ok { //如果s为Triangle类型,则ok为true
		fmt.Printf("三角形的边长为:%.2f,%.2f,%.2f, 周长为:%.2f,面积为:%.2f\n", ins.a, ins.b, ins.c, ins.peri(), ins.area())
	} else if ins, ok := s.(Cricle); ok {
		fmt.Printf("圆形的半径为:%.2f,周长为:%.2f,面积为:%.2f\n", ins.radius, ins.peri(), ins.area())
	} else if ins, ok := s.(*Triangle); ok {
		fmt.Printf("s: %T,%p\n", ins, ins)   //s: *main.Triangle,0xc0000c0040
		fmt.Printf("ins: %T,%p\n", ins, ins) //ins: *main.Triangle,0xc0000c0040
	}

}

//1.定义一个接口 表示形状的共有方法
type Shape interface {
	peri() float64 //形状的周长
	area() float64 //形状的面积
}

//2.定义实现类:三角形
type Triangle struct {
	a, b, c float64 //三角形边长
}

func (t Triangle) peri() float64 {
	return t.a + t.b + t.c
}

func (t Triangle) area() float64 {
	p := t.peri() / 2
	return math.Sqrt(p * (p - t.a) * (p - t.b) * (p - t.c))
}

type Cricle struct {
	radius float64 //半径
}

func (c Cricle) peri() float64 {
	return 2 * math.Pi * c.radius
}

func (c Cricle) area() float64 {
	return math.Pow(c.radius, 2) * math.Pi
}

type关键字

前面无论是定义结构体还是定义接口,我们都使用到了type关键字

是否思考过,type关键字究竟有什么作用呢?

type不是对应于c/c++的typedef的用法(定义类型别名),要搞清楚type就需要搞清楚go语言中的结构体、接口和函数等使用

package main

import (
	"fmt"
	"strconv"
	"time"
)

func main() {
	/*
			type: 用于定义类型或给类型起别名
				1.定义类型: type 类型名 Type
				2.类型别名:	type 类型名 = Type

			go中,可以使用type定义函数类型,来简化高阶函数中的代码复杂度

			注意:如果要给某个包下定义的类型拓展方法,那么只能使用定义新类型而不能使用别名,
		因为别名只能用于拓展本地(所在包)的定义的类型
	*/
	var i1 newint = 888
	var i2 int = 666
	var s1 newstr = "Jochen"
	var s2 string = "Amy"
	fmt.Printf("i1: %T, %d\n", i1, i1)
	fmt.Printf("i2: %T, %d\n", i2, i2)
	fmt.Printf("s1: %T, %s\n", s1, s1)
	fmt.Printf("s1: %T, %s\n", s2, s2)
	/*
		输出:
			i1: main.newint, 888
			i2: int, 666
			s1: main.newstr, Jochen
			s1: string, Amy
		可以看到,使用定义的类型,输出的类型也为定义的类型
	*/

	var str s = "hahah"
	var i i = 888
	//起别名起类型并不是新的类型,起本质上还是原来的类型
	fmt.Printf("str: %T, i: %T\n", str, i) //str: string, i: int

	//定义函数类型
	res := fun()(1, 2)
	fmt.Printf("%T,%v", res, res) //string,3

}

//1.定义新的类型
type newint int
type newstr string

//2.类型别名
type i = int //不是重新定义新的数据类型,而是给这个类型起小名
type s = string

//3.定义函数类型,简化代码复杂度
type myFun func(int, int) string

//高阶函数用法:将函数作为返回值 这样写就简化了代码复杂度
func fun() myFun {
	return func(a, b int) string {
		sum := a + b
		return strconv.Itoa(sum)
	}
}

//type MyDuration = time.Duration
type MyDuration time.Duration //改成这样就是本地定义的类型了
//非本地类型不能使用别名的方式地定义方法
func (m MyDuration) test() { //cannot define new methods on non-local type time.Duration

}

ps:类型别名是从go的1.9版本才新添加的功能,主要用于解决迁移中的类型兼容性问题(类型名变更问题)

错误和异常

开发过程中,经常需要通过程序的错误信息快速定位问题,go语言没有提供其他语言中的 try catch异常处理机制,而是通过函数返回值逐层往上抛出异常,这样的设计是因为鼓励工程师在代码去显式的检查错误,而不是去忽略它们,从而避免漏掉本应该处理的错误

但是过多的错误代码错误会带来代码丑长的问题,这也算是go语言的一大弊端吧

错误和异常实际上是两个概念:

错误:可能出现问题的地方出现了问题,是可预料、在意料之中的

异常:不应该出现问题的地方出现了问题,例如引用了空指针,是意料之外的事情

所以错误是业务过程的一部分,而异常不是

错误error

go语言中,错误是一种内置类型,其用error类型表示,错误值可以存储在变量中,从函数中返回

go语言没有提供其他语言中的 try catch异常处理机制,而是引用error这种错误标准类来作为错误处理的标准模式

牛刀小试

package main

import (
	"fmt"
	"log"
	"os"
)

func main() {
	/*
		错误的返回惯例:
			如一个函数或方法返回一个错误,那么它必须是函数的最后一个返回值

		处理错误的惯用方法:
			将返回的错误与nil比较,nil值表示没有错误,反之则有错误

	*/
	//演示错误:让程序打开一个不存在的文件
	//open函数表示打开一个指定的文件来进行读取,如果成功可以使用返回的文件指针进行读取,如果错误的话将返回路径错误
	f, err := os.Open("/不存在的一个文件路径.txt")
	if err != nil {
		log.Fatal(err) // open /不存在的一个文件路径.txt: no such file or directory
	}
	fmt.Println("文件成功打开", f.Name())

}

创建error

error的定义:

在go中error类型的定义如下:

type error interface{
	Error() string
}

它定义为一个接口类型,其中包含了Error()方法,该方法返回一个字符串,任何实现了这个接口的类型都可以作为一个错误使用,而Error方法的作用是为了提供对错误的表述信息

如何创建error类型:

  • go语言给我们提供了一个名为errors的包,该包下提供了New函数,该函数的返回值就是一个错误
  • go语言的fmt包下的Errorf函数可以格式化创建输出对象
package main

import (
	"errors"
	"fmt"
	"log"
)

func main() {
	/*
		error:是go的内置数据类型,其是一个接口,接口内部定义了: Error() string方法

		如何创建error对象
		使用go语言提供的errors包下的函数:New()可以创建一个error对象
		使用fmt包下的Errorf()函数
			Errorf(format string, a ...interface{}) error
	*/
	//1.创建一个error
	//1.1使用errors包创建
	err1 := errors.New("创建一个错误")
	fmt.Printf("%T,%v\n", err1, err1) //*errors.errorString,创建一个错误

	//1.2使用fmt包创建
	err2 := fmt.Errorf("又创建错误,错误码为:%d", 404)
	fmt.Printf("%T,%v\n", err2, err2) //*errors.errorString,又创建错误,错误码为:404

	err3 := checkAge(-1)
	if err3 != nil{
		log.Fatal(err3) //2021/02/11 05:22:53 年龄-1,为负数,想上天?
	}
	//发生错误,代码会在错误处中断,不会往下执行
	fmt.Println("程序继续")
}

//设计函数:验证年龄是否合法,如果为负数则返回错误信息
func checkAge(age int) error {
	if age < 0 {
		//返回error对象
		err := fmt.Errorf("年龄%d,为负数,想上天?", age)
		return err
	}
	fmt.Println("年龄是:", age)
	return nil
}

获取错误底层结构类型

通常,我们会使用fmt.Println()打印错误信息,其实该函数内部会去调用错误类型的Error方法来获得错误的描述信息,这样的信息一般比较简短的

通过上面的学习我们知道,任何实现了error接口的结构类型都可以作为一个错误使用。所以,所有的错误必然都有一个底层结构类型。该结构包含有所有错误信息的描述,可能会暴露更多的方法或者字段去获取错误下信息。对于底层结构,一般都需要通过断言去获取

对于内置的函数错误结构往往包含除了Error方法外其他的方法或者成员字段去描述更多错误信息,我们也可以通过断言获取底层结构类型(不知道是什么结构就点进去看看源码),获取更多的错误信息,如下例子:获取os包下的open函数的错误结构类型的例子:

package main

import (
	"fmt"
	"os"
)

func main() {
	f, err := os.Open("/不存在的一个文件路径.txt")
	if err != nil {
		fmt.Println(err) // open /不存在的一个文件路径.txt: no such file or directory
		//open函数的错误是通过PathError这个结构体存储的
		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.op: open
				2.Path: /不存在的一个文件路径.txt
				3.Err: no such file or directory
			*/
		}
		return
	}
	fmt.Println("文件成功打开", f.Name())

}

自定义error

前面我们有学习过两种创建自定义错误的函数

  • errors包下的New函数
  • fmt包下的Errorf函数

这两个函数内部实际上都维护了一个底层的错误结构类型。一个结构实现类error接口(即实现Error()方法),那个这个结构就被认为是error类型

所以我们还可以自己去定义一个结构体,实现error接口去自定义我们的错误

package main

import (
	"fmt"
	"math"
)

func main() {
	/*
		自定义error
	*/
	if area, err := circleArea(-100); err != nil {
		fmt.Println(err) //error:半径,-100.00, 半径小于0,想上天?
		//使用断言获取错误信息类型的数据
		if e, ok := err.(*areaError); ok {
			fmt.Println("mes:", e.msg)
			fmt.Println("radius:", e.radius)
		}
	} else {
		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(r float64) (float64, error) {
	if r < 0 {
		return 0, &areaError{msg: "半径小于0,想上天?", radius: r}
	}
	return math.Pi * math.Pow(r, 2), nil
}

一般Error错误的描述会比较简洁笼统,会更多的考虑通用性。此时我们也可以拓展更多的自定义的错误方法,方便对外判断具体是哪个地方出现的问题,提供更多的错误信息

package main

import (
	"fmt"
)

func main() {
	/*
		自定义error
	*/
	if area, err := rectArea(-1, -2); err != nil {
		fmt.Println(err) //error:半径,-100.00, 半径小于0,想上天?
		//使用断言获取错误信息类型的数据
		if e, ok := err.(*areaError); ok {
			//通过自定义的错误方法去判断哪里出的错误,然后自己去输出错误内容
			if e.lenNegative() {
				fmt.Printf("error:长度:%.2f,小于0", e.len)
			}
			if e.widthNegative() {
				fmt.Printf("error:宽度:%.2f,小于0", e.width)
			}
		}
	} else {
		fmt.Println("长方形的面积为:", area)
	}

}

//1.定义一个结构体,表示错误的类型
type areaError struct {
	msg   string  //错误内容
	len   float64 //发生错误时,矩形的长度
	width float64 //发生错误时,矩形的宽度
}

//2.实现error接口,就是实现Error()方法
func (e *areaError) Error() string { //使用指针类型节省空间,因为结构体是值传递
	return fmt.Sprintf("error:长度,%.2f,宽度,%.2f, %s", e.len, e.width, e.msg) //格式化获取字符串
}

//长度非法
func (e *areaError) lenNegative() bool {
	return e.len < 0
}

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

//获取矩形的面积,返回面积和错误信息
func rectArea(l, w float64) (float64, error) {
	msg := ""
	if l < 0 {
		msg += "长度为负数,想上天? "
	} else if w < 0 {
		msg += "宽度为负数,想上天? "
	}

	if msg != "" {
		return 0, &areaError{msg: msg, len: l, width: w}
	}
	return l * w, nil
}

panic()和recover()

go语言提倡使用返回值去返回错误,而非像其他语言一样,通过try catch去不加区分的抛出错误和异常

前面我们介绍了对于错误的处理,在程序中,往往会发生我们不可预知(业务之外)的“程序异常”,这种不可预知的情况我们是没办法去写好错误信息去返回的,这时就需要使用对应的异常处理机制

go语言提供了panic和recover两个函数来触发和终止程序异常处理的流程,同时引用defer关键字去延迟执行defer后的函数

通过panic可以抛出异常,然后在defer中通过recover捕获这个异常,就可以进行异常处理了

panic(恐慌):

  • 内置函数panic,该函数会终止后续代码的执行
  • 若函数中有多个defer语句,那么发生恐慌后,"已经"被defer函数按定义的顺序逆序执行(后defer的先执行,先defer的后执行)
  • 若发生恐慌的函数是被调用函数,则调用处往下的代码也会终止,调用处的defer片段也会按定义逆序执行,多层调用也是如此,整个协程(goroutine结束),将错误报告而出
package main

import "fmt"

func main() {
	/*
		go语言通过panic()和recover()实现程序中特殊的异常处理
			panic:让程序进入恐慌,中断执行
			revover:让程序恢复,必须在defer函数中使用
	*/
	A()
	defer println("第一次defer主函数")
	B()
	//函数B发生恐慌,代码在调用处终止往下执行,前面被defer的函数会执行,后面的不会执行
	/*
		print:
			A!
			B!
			1
			2
			3
			4
			第一次defer函数B
			第一次defer主函数
			panic: B慌了

			goroutine 1 [running]:
			main.B()
					/home/GoWorkSpace/src/MyRecored/25_panic&recover/panic1.go:30 +0x1a6
			main.main()
					/home/GoWorkSpace/src/MyRecored/25_panic&recover/panic1.go:13 +0x98

	*/
	defer println("第二次defer主函数")

	fmt.Println("主函数拉闸")
}
func A() {
	println("A!")
}

func B() { //对于函数B中的defer函数来说,函数B被称为外围函数
	println("B!")
	defer println("第一次defer函数B")
	for i := 1; i <= 8; i++ {
		fmt.Println(i)
		if i == 4 {
			//让程序中断
			panic("B慌了")
			//后续代码不会执行,上面已经被defer的函数会被执行,下面还没defer还没被执行,所以不执行
		}
	}
	//当外围函数的代码中发生了运行恐慌,只有其中所有已经被defer的函数全部执行结束后,运行时恐慌才会被真正拓展至调用处
	defer println("第二次defer函数B")
}

recover(恢复):

  • 内置函数recover,用来控制一个协程的panicking行为,用以捕获panic从而影响应用的行为
  • 在defer函数中,通过recover来终止一个恐慌,让程序恢复正常执行,也可以利用panic去传递error信息
package main

import "fmt"

func main() {
	/*
		go语言通过panic()和recover()实现程序中特殊的异常处理
			panic:让程序进入恐慌,中断执行
			revover:让程序恢复,必须在defer函数中使用
	*/
	A()
	defer println("第一次defer主函数")
	B()
	defer println("第二次defer主函数")

	fmt.Println("主函数拉闸")
	/*
		print:
			1
			2
			3
			4
			第一次defer函数B
			程序发生恐慌,你注意一下,原因:B慌了。程序不会中断,不会向调用处抛出恐慌
			主函数拉闸
			第二次defer主函数
			第一次defer主函数

	*/
}
func A() {
	println("A!")
}

func B() { //对于函数B中的defer函数来说,函数B被称为外围函数
	defer func() {
		mes := recover() // 返回的是panic中传递的从参数
		if mes != nil {
			//发生恐慌
			fmt.Printf("程序发生恐慌,你注意一下,原因:%s。程序不会中断,不会向调用处抛出恐慌\n", mes)
		}
	}()
	println("B!")
	defer println("第一次defer函数B")
	for i := 1; i <= 8; i++ {
		fmt.Println(i)
		if i == 4 {
			//让程序中断
			panic("B慌了")
			//recover处理了恐慌,所以程序不会向上抛出异常,但是恐慌发生后面的代码不会恢复被执行了
		}
	}
	fmt.Println("发生恐慌被恢复后,B函数不会恢复往下执行,只不过不向外抛出恐慌罢了")
	defer println("第二次defer函数B")
}


在go中错误和异常其实就是error和panic的区别,什么时候用错误表达式,什么时候用异常需要有一套规则,否则很容易出现一些皆错误或异常的情况

一般情况下异常处理的常用有如下:

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

ps:对于异常,应该选择在一个合适的上游去recover,并打印堆栈信息,使程序不会终止运行

其他情况,就使用错误处理

学习资料参考:这里

posted @ 2021-02-10 23:15  .Jochen  阅读(241)  评论(0编辑  收藏  举报