【GO】二、函数、结构体与错误处理

函数

go 语言中的函数支持 匿名函数、闭包,且其具有一等公民的特性:函数本身可以被当做变量进行传递

简单示例:

func add (a int, b int) (int, error) {
	return a + b, nil
}

func main() {
	fmt.Println(add(1, 1))
}

注意函数之间参数的传递都是值传递

可变形参:

func add(items ...int) (sum int, err error) {
	for _, value := range items {
		sum += value
	}
	return sum, nil
}

func main() {
	fmt.Println(add(1, 1, 2, 3, 777))
}

可变形参在传递时会被当做一个 Slice 进行传递

一等公民特性:

函数可以作为参数进行传递,我们可以在函数的形参中定义函数,来将一个设定好的函数传入,并控制它的调用时机,令程序更加灵活

闭包

闭包可以做到将函数分成两半,让其中一半先执行,另一半过一段时间再执行

也就是说,其返回一个函数,我们在调用的时候也需要使用一个变量来接收函数

之后我们可以通过这个变量来调用这个后一半被存储的函数

/**
 * 返回值是一个函数,我们可以自己选择函数的执行时间点
 */
func addEveryTime() func() int {
	local := 0
	return func() int {
		local += 1
		return local
	}
}

func main() {
	fmt.Println(add(1, 1, 2, 3, 777))
	// 该语句执行结束之后,变量local就被存储在内存中了,我们执行addEveryTime函数但不执行真正的数字增加的任务
	f := addEveryTime()
	// 调用这个函数,每次都加 1
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
}

相当于,先创建变量,将对变量的操作放在之后进行

defer

defer 是一个关键词,相当于 finally,但 golang 考虑到 finally 语句和业务语句相隔太远,可能会产生遗忘,所以defer关键词的作用是将这个语句在 Return 之前执行:

func main() {
	defer fmt.Println("我是第一个 defer 我被压在栈低,我最后执行")
	defer fmt.Println("我是第二个 defer 我被压在栈顶,我在中间执行")
	fmt.Println("我没有使用defer,我第一个执行")
	return
	/**
	我没有使用defer,我第一个执行
	我是第二个 defer 我被压在栈顶,我在中间执行
	我是第一个 defer 我被压在栈低,我最后执行
	*/
}

对于 defer 的执行顺序来讲:它是一个栈的概念,我们先定义的 defer 会被压栈,在全部的defer 压栈结束之后,会一点点进行弹出

另外 defer 是可以处理返回值的:

func deferTest() (res int) {
	defer func() {
		res++
	}()
	return 10
}

func main() {
	dd := deferTest()
	fmt.Println(dd)
	// 11
}

也就是说,return 中的内容也是优先于 defer 执行的,defer 在函数退出之前执行

错误处理

go 语言中 错误处理相关的有三种:error、panic、recover

error

Go 语言认为:不应该使用 try - catch 来包住某个代码块来进行错误异常处理,而是应该返回一个 error 变量,若 error 为 nil,则认为程序 没有出错

另外,Go语言中不存在向上抛出异常等机制,Go语言认为所有的 error 都必须进行处理,故代码会变得十分啰嗦,但另一方面,这也带来了更加健壮的程序,这叫做 防御性编程

下面是一个经典的处理 error 的例子:

func A() (int, error) {
	return 0, errors.New("This is a test error")
}

func main() {
	// 若有错误
	if _, err := A(); err != nil {
		fmt.Println(err)
	}
}

panic & recover

panic 会导致程序中止,进而程序退出,故我们一般不使用panic,并且我们也要避免程序被动的触发 panic

一个经典的触发 panic 的情况:

	var mapTest map[string]string
	mapTest["123"] = "46"

我们可以使用recover 在panic触发之前及时阻止程序的退出:

func main() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("recover if A ", r)
		}
	}()
	var mapTest map[string]string
	mapTest["123"] = "46"
}

结构体

Go 语言中没有类的概念,其使用接口体来实现面向对象的思想

Type

type 允许我们为类型定义一个别名:

func main() {
	type MyInt666666 = int
	var test MyInt666666
	fmt.Printf("%T\r\n", test) // int
}

但 test 变量 本质上还是一个 int,代码在编译时会直接被替换为 int

另外的 type 关键字更重要的作用是创建一个自定义的数据类型,这个数据类型是基于已有类型的,但我们可以为其拓展方法:

例如这里我们就拓展了一个 toString() 的方法

type MyInt int

func (my MyInt) toString() string {
	return strconv.Itoa(int(my))
}

func main() {
	var test MyInt = 6
	var testString string = test.toString()
	fmt.Printf("%T\r\n", test)       // main.MyInt
	fmt.Printf("%T\r\n", testString) // string
}

结构体

结构体的定义与使用示例:

type Person struct {
	name    string
	age     int
	address string
	height  float32
}

func main() {
	// 另外,对结构体的赋值与取值是十分方便的
	var p Person
	p.name = "张家硕"
	fmt.Println(p.name)
}

匿名结构体:

只临时使用,不做统一定义就这么写:

	tempStruct := struct {
		province string
		city     string
		num      int
	}{
		province: "河北",
		city:     "石家庄",
		num:      10086,
	}
	fmt.Println(tempStruct.province)

结构体的嵌套:

嵌套方式一:显示指明嵌套的名称,这样我们取属性就只能 对象.内层对象.属性 这样取数据

type Person struct {
	name string
	age  int
}

type Student struct {
	p     Person // 第一种定义方式,在取值时会略有繁琐
	grade float32
}

func main() {
	xiaoming := Student{
		p: Person{
			name: "小明",
			age:  15,
		},
		grade: 98.0,
	}
	fmt.Println(xiaoming.p.name)
}

第二种嵌套:

这种也叫做匿名嵌入的方式,这种方式中,我们取数会更加方便,但我们存储时,若要存储对象,则会将对象存储回去,但前提是对象中不能重名,也就是内嵌的结构体中的属性若与外部的属性重名,这种方式就会更改外部的属性

type Person struct {
	name string
	age  int
}

type Student struct {
	Person // 第二种嵌套方式,取值更加简单,但其赋值操作也更为繁琐
	grade  float32
}

func main() {
	xiaoming := Student{
		Person{
			name: "小明",
			age:  15,
		},
		98.0,
	}
	fmt.Println(xiaoming.name)
}

向结构体添加方法:

例如添加一个打印的方法:

type Person struct {
	name string
	age  int
}

// 此处要注意的问题是:我们将一个 Person 对象传入时,这是一个值传递,也就是说我们更改不会作用于原对象,我们要使用指针将其修改为引用传递才能对原对象进行修改
func (person Person) toString() {
	fmt.Println(person.name, person.age)
}

func main() {
	xiaoming := Person{
		name: "小明",
		age:  18,
	}
	xiaoming.toString()
}

另外,我们就算在不进行修改的情况下,也可以通过尽可能使用指针的方式,这种方式不对对象进行复制,会更加节省空间

func (person *Person) toString()

此时,我们可以通过实体对象直接调用这个方法,后台会帮助我们将它自动转换成指针相关,我们也可以将对象先取地址再调用这个方法。

	xiaoming := &Person{
		name: "小明",
		age:  18,
	}
	xiaoming.toString()

指针

指针反映的是对象在内存中存储的地址,同时也可以看做是一种数据类型,每种数据类型都有其对应的指针数据类型,包括我们自定义的结构体也是

type Person struct {
	name string
	age  int
}

func changeName(person *Person) {
	person.name = "修改过的名字"
}

func main() {
	xiaoming := Person{
		name: "小明",
		age:  25,
	}
	changeName(&xiaoming)
	fmt.Println(xiaoming.name)
}

这里要注意一个地方,我们fmt.Println 在打印指针的时候可以直接打印出内容是因为 Println 对地址的输出进行了优化

此处:

p := Person{...}	// 是创建了一个值对象
p := Person{...}	// 是创建了一个指针对象

但 Go 语言针对于指针的优化是:

type Person struct {
	name string
	age  int
}

func changeName(person *Person) {
	person.name = "修改过的名字"
}

func main() {
	xiaoming := &Person{
		name: "小明",
		age:  25,
	}
	changeName(xiaoming)
	// 就算我们是一个指针对象,该指针对象也可以直接调用对象内部的属性
	fmt.Println(xiaoming.name)
	// 常规来讲我们应该以下面这种方式调用,对于地址型变量来讲,在前面加上取指针符号,就会变成原有的值对象
	fmt.Println((*xiaoming).name)
}

另外的:Go 语言中禁用了指针的运算

更进一步的,也可以使用 Go 语言中的 unsafe 包来强行使用指针的运算,但这一般不被鼓励使用

注意:我们不能对一个空的指针对象进行取值,会直接抛出空指针异常

var p1 Person		// 这种声明方式会创建对象,只不过其中的元素都是初值内容
var p2 *Person		// 通过指针进行声明的话,这个对象不会被创建,我们直接调用其中内容会抛出空指针异常,我们需要创建对象才行
	var p *Person
	p = &Person{}
	fmt.Println(p)		&{ 0}
	// 第一种初始化的方法
	var p *Person
	p = &Person{}

	// 第二种初始化方法
	var emptyPerson Person
	pi := &emptyPerson

	// 第三种初始化方法
	var pp = new(Person) // var pp *Person = new(Person)

对于map、channel、slice 可以使用 make 方法,而对于指针,则需要使用 new 方法进行初始化

nil 的理解

例如我们判断一个slice 是否为 nil 时,其实是判断 slice 中的 element 是否为 nil 。

当一个指针被声明了,但没有初始化时,这个指针的值就是nil

我们使用 var xxx map[xxx]xxx 时,就是对map进行了声明,但没有初始化,map 会变为 nil

但我们使用var xxx = make(map[xxx]xxx) 时,就是对其初始化了,其不为 nil,我们称其为空 map

接口

go 语言中,处处都是 interface、处处都是 鸭子类型

go 语言中 不需要显示的对接口进行 继承操作,内部会自动识别

鸭子类型强调的更多是接口的方法,而不是接口的属性与结构

接口是一个声明了很多方法的集合,我们的对象如果要实现这个接口,就必须实现这个接口中的所有方法

type Duck interface {
	Walk()
	Swim()
}

type kedaDuck struct {
	color string
}

func (kd *kedaDuck) Walk() {
	fmt.Println("Walkkkkk")
}

func (kd *kedaDuck) Swim() {
	fmt.Println("Swiiiiiiim")
}

func main() {
	// 多态,声明一个对象,这个对象是一个Duck,但他实际上是一个kedaDuck
	
	
	var dd Duck = &kedaDuck{
		color: "yellow",
	}
	dd.Walk()
}

注意这里要实现的接口的接口体是必须实现所有在接口中声明的方法的。

断言

我们可以将interface视为一个数据类型,这个数据类型可以接收任意类型的数据,但是我们如果要对这个数据类型加以加工的话,就需要我们使用断言的方式将其转换为另一种数据类型再进行操作

这种方式存在的问题就是,我们将数据类型传入之后,难以对其进行操作,这里引入一种方式:

func add(a, b interface{}) interface{} {
	switch a.(type) {
	case int:
		return a.(int) + b.(int)
	case float64:
		return a.(float64) + b.(float64)
	default:
		panic("unsupported type")
	}
}

func main() {
	a, b := 1.0, 2.0
	fmt.Println(add(a, b))
}

接口的多实现(继承)

接口的多实现:

type MyWriter interface {
	Write(string)
}

type MyReader interface {
	Read() string
}

// 定义一个接口 实现上面两个接口
type MyReadWrite interface {
	MyWriter
	MyReader
	ReadWrite()
}

你可能感兴趣的:(Go,golang,开发语言,后端)