GO语言学习(四)

函数

函数和方法是我们迈向代码复用、多人协作开发的第一步。通过函数,可以把开发任务分解成一个个小的单元,这些小单元可以被其他单元复用,进而提高开发效率、降低代码重合度。再加上现成的函数已经被充分测试和使用过,所以其他函数在使用这个函数时也更安全,比你自己重新写一个相似功能的函数 Bug 率更低

以main函数为例:

  • 任何一个函数的定义,都有一个 func 关键字,用于声明一个函数,就像使用 var 关键字声明一个变量一样;
  • 然后紧跟的 main 是函数的名字,命名符合 Go 语言的规范即可,比如不能以数字开头;
  • main 函数名字后面的一对括号 () 是不能省略的,括号里可以定义函数使用的参数,这里的 main 函数没有参数,所以是空括号 () ;
  • 括号 () 后还可以有函数的返回值,因为 main 函数没有返回值,所以这里没有定义;
  • 最后就是大括号 {} 函数体了,你可以在函数体里书写代码,写该函数自己的业务逻辑。

函数声明

func funcName(params) result {
    body
}

这就是一个函数的签名定义,它包含以下几个部分:

  1. 关键字 func;
  2. 函数名字 funcName;
  3. 函数的参数 params,用来定义形参的变量名和类型,可以有一个参数,也可以有多个,也可以没有;
  4. result 是返回的函数值,用于定义返回值的类型,如果没有返回值,省略即可,也可以有多个返回值;
  5. body 就是函数体,可以在这里写函数的代码逻辑。
package main

import "fmt"

func main() {
	a := 1
	b := 2
	i := sum(a, b)
	fmt.Println(i)
}

func sum(a int, b int) int {
	return a + b
}

这是一个计算两数之和的函数,函数的名字是 sum,它有两个参数 a、b,参数的类型都是 int。sum 函数的返回值也是 int 类型,函数体部分就是把 a 和 b 相加,然后通过 return 关键字返回,如果函数没有返回值,可以不用使用 return 关键字

多值返回

同java不一样,Go 语言的函数可以返回多个值,也就是多值返回

func sum(a, b int) (int, error) {
	if a < 0 || b < 0 {
		return 0, errors.New("a或者b不能是负数")
	}
	return a + b, nil
}

提示:这里使用的 error 是 Go 语言内置的一个接口,用于表示程序的错误信息

命名返回参数

不止函数的参数可以有变量名称,函数的返回值也可以,也就是说你可以为每个返回值都起一个名字,这个名字可以像参数一样在函数体内使用

package main

import (
	"errors"
	"fmt"
)

func main() {
	a := -1
	b := 2
	i, err := sum2(a, b)
	fmt.Println(i, err)
}
func sum2(a, b int) (sum int, err error) {
	if a < 0 || b < 0 {
		return 0, errors.New("a或者b不能是负数")
	}
	sum = a + b
	err = nil
	return
}

返回值的命名和参数、变量都是一样的,名称在前,类型在后。以上示例中,命名的两个返回值名称,一个是 sum,一个是 err,这样就可以在函数体中使用它们了

可变参数

可变参数与java中类似

func sum3(params ...int) int {
    sum := 0
    for _, i := range params {
        sum += i
    }
    return sum
}

这里需要注意,如果你定义的函数中既有普通参数,又有可变参数,那么可变参数一定要放在参数列表的最后一个,比如 sum1(tip string,params …int) ,params 可变参数一定要放在最末尾

包级函数

不管是自定义的函数 sum、sum1,还是我们使用到的函数 Println,都会从属于一个包,也就是 package。sum 函数属于 main 包,Println 函数属于 fmt 包

  1. 函数名称首字母小写代表私有函数,只有在同一个包中才可以被调用;
  2. 函数名称首字母大写代表公有函数,不同的包也可以调用;
  3. 任何一个函数都会从属于一个包。

小提示:Go 语言没有用 public、private 这样的修饰符来修饰函数是公有还是私有,而是通过函数名称的大小写来代表,这样省略了烦琐的修饰符,更简洁。

匿名函数和闭包

顾名思义,匿名函数就是没有名字的函数,这是它和正常函数的主要区别

在下面的示例中,变量 sum2 所对应的值就是一个匿名函数。需要注意的是,这里的 sum2 只是一个函数类型的变量,并不是函数的名字

func main() {
    sum2 := func(a, b int) int {
        return a + b
    }
    fmt.Println(sum2(1, 2))
}

通过 sum2,我们可以对匿名函数进行调用,以上示例算出的结果是 3,和使用正常的函数一样。

有了匿名函数,就可以在函数中再定义函数(函数嵌套),定义的这个匿名函数,也可以称为内部函数。更重要的是,在函数内定义的内部函数,可以使用外部函数的变量等,这种方式也称为闭包

func main() {
    cl:=colsure()
    fmt.Println(cl())
    fmt.Println(cl())
    fmt.Println(cl())
}

func colsure() func() int {
    i:=0
    return func() int {
        i++
        return i
    }
}

输出结果:

1
2
3

这都得益于匿名函数闭包的能力,让我们自定义的 colsure 函数,可以返回一个匿名函数,并且持有外部函数 colsure 的变量 i。因而在 main 函数中,每调用一次 cl(),i 的值就会加 1

小提示:在 Go 语言中,函数也是一种类型,它也可以被用来声明函数类型的变量、参数或者作为另一个函数的返回值类型。

方法

不同于函数的方法

在 Go 语言中,方法和函数是两个概念,但又非常相似,不同点在于方法必须要有一个接收者,这个接收者是一个类型,这样方法就和这个类型绑定在一起,称为这个类型的方法

在下面的示例中,type Age uint 表示定义一个新类型 Age,该类型等价于 uint,可以理解为类型 uint 的重命名。其中 type 是 Go 语言关键字,表示定义一个类型,在结构体和接口的课程中我会详细介绍。

func main() {
	age := Age(25)
	age.String()
}

type Age uint

func (age Age) String() {
	fmt.Println("the age is", age)
}

示例中方法 String() 就是类型 Age 的方法,类型 Age 是方法 String() 的接收者。

和函数不同,定义方法时会在关键字 func 和方法名 String 之间加一个接收者 (age Age) ,接收者使用小括号包围。

接收者的定义和普通变量、函数参数等一样,前面是变量名,后面是接收者类型。

现在方法 String() 就和类型 Age 绑定在一起了,String() 是类型 Age 的方法

提示:因为 25 也是 unit 类型,unit 类型等价于我定义的 Age 类型,所以 25 可以强制转换为 Age 类型。

值类型接收者和指针类型接收者

方法的接收者除了可以是值类型(比如上一小节的示例),也可以是指针类型

定义的方法的接收者类型是指针,所以我们对指针的修改是有效的,如果不是指针,修改就没有效果,如下所示:

func (age *Age) Modify(){
    *age = Age(30)
}

调用一次 Modify 方法后,再调用 String 方法查看结果,会发现已经变成了 30,说明基于指针的修改有效,如下所示:

age:=Age(25)
age.String()
age.Modify()
age.String()

提示:在调用方法的时候,传递的接收者本质上都是副本,只不过一个是这个值副本,一是指向这个值指针的副本。指针具有指向原有值的特性,所以修改了指针指向的值,也就修改了原有的值。我们可以简单地理解为值接收者使用的是值的副本来调用方法,而指针接收者使用实际的值来调用方法。

示例中调用指针接收者方法的时候,使用的是一个值类型的变量,并不是一个指针类型,其实这里使用指针变量调用也是可以的,如下面的代码所示:

(&age).Modify()

这就是 Go 语言编译器帮我们自动做的事情

  • 如果使用一个值类型变量调用指针类型接收者的方法,Go 语言编译器会自动帮我们取指针调用,以满足指针接收者的要求。
  • 同样的原理,如果使用一个指针类型变量调用值类型接收者的方法,Go 语言编译器会自动帮我们解引用调用,以满足值类型接收者的要求。

总之,方法的调用者,既可以是值也可以是指针,不用太关注这些,Go 语言会帮我们自动转义,大大提高开发效率,同时避免因不小心造成的 Bug。

不管是使用值类型接收者,还是指针类型接收者,要先确定你的需求:在对类型进行操作的时候是要改变当前接收者的值,还是要创建一个新值进行返回?这些就可以决定使用哪种接收者。

方法是否可以赋值给一个变量?如果可以,要怎么调用它呢?答案是完全可以,方法赋值给变量称为方法表达式,如下面的代码所示

age:=Age(25)
//方法赋值给变量,方法表达式
sm:=Age.String
//通过变量,要传一个接收者进行调用也就是age
sm(age)

结构体

结构体定义

结构体是一种聚合类型,里面可以包含任意类型的值,这些值就是我们定义的结构体的成员,也称为字段。在 Go 语言中,要自定义一个结构体,需要使用 type+struct 关键字组合

在下面的例子中,我自定义了一个结构体类型,名称为 person,表示一个人。这个 person 结构体有两个字段:name 代表这个人的名字,age 代表这个人的年龄。

type person struct {
	name string
	age  uint
}

在定义结构体时,字段的声明方法和平时声明一个变量是一样的,都是变量名在前,类型在后,只不过在结构体中,变量名称为成员名或字段名。

其中:

  • type 和 struct 是 Go 语言的关键字,二者组合就代表要定义一个新的结构体类型。
  • person 是结构体类型的名字。
  • name 是结构体的字段名,而 string 是对应的字段类型。
  • 字段可以是零个、一个或者多个。

结构体声明使用

p2 := person{name: "laowan", age: 18}   //声明一个person类并赋值
var p person //声明一个空的类型 

在 Go 语言中,访问一个结构体的字段和调用一个类型的方法一样,都是使用点操作符“.”

字段结构体

结构体的字段可以是任意类型,也包括自定义的结构体类型,比如下面的代码:

func main() {
	p2 := person{name: "laowan", age: 18, address: address{"湖南", "长沙"}}
	fmt.Println(p2)
}
type person struct {
	name    string
	age     uint
	address address
}

type address struct {
	province string
	city     string
}

在这个示例中,我定义了两个结构体:person 表示人,address 表示地址。在结构体 person 中,有一个 address 类型的字段 addr,这就是自定义的结构体

接口

接口的定义

接口是和调用方的一种约定,它是一个高度抽象的类型,不用和具体的实现细节绑定在一起。接口要做的是定义好约定,告诉调用方自己可以做什么,但不用知道它的内部实现,这和我们见到的具体的类型如 int、map、slice 等不一样

type animal interface {
	eat() string
	sleep() string
}

和java一样,接口只负责出参与入参,置于其中的实现由实现者完成

接口的实现

接口的实现者必须是一个具体的类型,继续以 person 结构体为例,让它来实现 animal 接口,如下代码所示:

func main() {
	p := person{"laowan", 18, address{"湖南", "长沙"}}
	fmt.Println(p.eat())
}

type animal interface {
	eat() string
	sleep() string
}

func (p person) eat() string {
	return fmt.Sprintf("%s 在 %s省%s市 eat", p.name, p.address.province, p.address.city)
}

type person struct {
	name    string
	age     uint
	address address
}

type address struct {
	province string
	city     string
}

声明一个接口animal,里面定义了eat和sleep方法,然后使用person类实现此接口,实现其中的业务逻辑

值接收者和指针接收者

我们已经知道,如果要实现一个接口,必须实现这个接口提供的所有方法,而且在上篇文章说方法的时候,我们也知道定义一个方法,有值类型接收者和指针类型接收者两种。二者都可以调用方法,因为 Go 语言编译器自动做了转换,所以值类型接收者和指针类型接收者是等价的。但是在接口的实现中,值类型接收者和指针类型接收者不一样,下面我会详细分析二者的区别

可以这样解读:

  • 当值类型作为接收者时,person 类型和*person类型都实现了该接口。
  • 当指针类型作为接收者时,只有*person类型实现了该接口。

可以发现,实现接口的类型都有*person,这也表明指针类型比较万能,不管哪一种接收者,它都能实现该接口

工厂函数

工厂函数一般用于创建自定义的结构体,便于使用者调用,我们还是以 person 类型为例,用如下代码进行定义

func NewPerson(name string) *person {
    return &person{name:name}
}

我定义了一个工厂函数 NewPerson,它接收一个 string 类型的参数,用于表示这个人的名字,同时返回一个*person。

通过工厂函数创建自定义结构体的方式,可以让调用者不用太关注结构体内部的字段,只需要给工厂函数传参就可以了

用下面的代码,即可创建一个*person 类型的变量 p1

func main() {
	p1 := NewPerson("张三")
	fmt.Println(p1)
}

func NewPerson(name string) *person {
	return &person{name: name}
}

工厂函数也可以用来创建一个接口,它的好处就是可以隐藏内部具体类型的实现,让调用者只需关注接口的使用即可。

继承和组合

在 Go 语言中没有继承的概念,所以结构、接口之间也没有父子关系,Go 语言提倡的是组合,利用组合达到代码复用的目的,这也更灵活

我同样以 Go 语言 io 标准包自带的接口为例,讲解类型的组合(也可以称之为嵌套),如下代码所示

type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}
//ReadWriter是Reader和Writer的组合
type ReadWriter interface {
    Reader
    Writer
}

ReadWriter 接口就是 Reader 和 Writer 的组合,组合后,ReadWriter 接口具有 Reader 和 Writer 中的所有方法,这样新接口 ReadWriter 就不用定义自己的方法了,组合 Reader 和 Writer 的就可以了。

类型断言

有了接口和实现接口的类型,就会有类型断言。类型断言用来判断一个接口的值是否是实现该接口的某个具体类型。

func main() {
	p := person{"laowan", 18, address{"湖南", "长沙"}}
	var a animal
	a = &p
	p2 := a.(*person)
	fmt.Println(p2)
}

type animal interface {
	eat() string
	sleep() string
}

func (p person) eat() string {
	return fmt.Sprintf("%s 在 %s省%s市 eat", p.name, p.address.province, p.address.city)
}

func (p *person) sleep() string {
	return fmt.Sprintf("%s 在 %s省%s市 sleep", p.name, p.address.province, p.address.city)
}

type person struct {
	name    string
	age     uint
	address address
}

type address struct {
	province string
	city     string
}

输出结果:&{laowan 18 {湖南 长沙}}

如上所示,声明了一个animal接口,提供内部方法,person去实现这个接口,然后声明一个person变量,赋值给

animal,然后使用类型断言表达式 a.(*person),尝试返回一个 p2,如果接口的值 a 是一个*person,那么类型断言正确,可以正常返回 p2。如果接口的值 a 不是一个 *person,那么在运行时就会抛出异常,程序终止运行。

小提示:这里返回的 p2 已经是 *person 类型了,也就是在类型断言的时候,同时完成了类型转换。

p := person{"laowan", 18, address{"湖南", "长沙"}}
	var a animal
	a = &p
	p2, ok := a.(*person)
	if ok {
		fmt.Println(p2)
	} else {
		fmt.Println("a不是一个person")
	}

	p3, ok1 := a.(*address)
	if ok1 {
		fmt.Println(p3)
	} else {
		fmt.Println("a不是一个address")
	}

输出结果:

&{laowan 18 {湖南 长沙}}
a不是一个address

posted @ 2022-06-22 15:35  小学程序员  阅读(127)  评论(0编辑  收藏  举报