引言
对于任何一门语言来说,函数是最基本的功能模块。如果让我用一句话描述函数,那一定是:根据某些输入,执行某些任务,返回某些输出的一等公民。那函数的参数具有哪些特性?返回值具有哪些特性?一等公民具有哪些特定?利用函数一等公民的特性,我们能解决什么问题?带这这些疑问,我们来一探究竟;
一、函数原型
go
func Fprintln(w io.Writer, a ...any) (n int, err error) {
...
return
}
上述函数来自于fmt
包,包含了函数的大部分特性。很明显,一个函数包含:
- 函数签名 (Fprintln),Go语言不支持函数重载,因此包维度函数名唯一;
- 参数:固定参数以及可变参数,Go语言不支持默认参数值,如果需要实现该功能,需要根据参数默认值在代码逻辑中实现;
- 返回值:支持返回多个返回值,并能够给给出具名返回值(也就是在函数签名中定义了返回值变量)
可变参数
上述几部分,都比较容易理解,这里注重讲解一下可变参数的用法。要理解可变参数,需要知道这几个点:
- 调用含有可变参数的函数,可以传0个,1个,多个参数,如果你传递的是一个
slice []
,需要使用...slice
。 - 在被调用函数中,使用
slice
来接数据。
通过一个例子,来看一下:
go
func getSomething(a int, b ...int) {
fmt.Printf("b's type:[%T], len:[%v] \n", b, len(b))
fmt.Println(a, b)
}
func TestGet(t *testing.T) {
// 传递0个
a := 1
getSomething(a) // b's type:[[]int], len:[0]
// 传递1个
b := 1
getSomething(a, b) // b's type:[[]int], len:[1]
// 传递切片
b1 := []int{1, 3}
getSomething(a, b1...) // b's type:[[]int], len:[2]
}
Option设计模式
在实现功能的时候,对于程序员来说,一定要考虑可扩展性的。可扩展性最基本的一条,对外暴露的协议,不到万不得已不要修改。我们可以采用可变参数来减少接口的变动。这里对比了三种构建方法:
方式一:
go
func NewComplexObj1(name string, age int, address string, sex int) *ComplexObj {
return &ComplexObj{
name: name,
age: age,
address: address,
sex: sex,
}
}
后续当需要增加字段时,这个接口需要增加参数。
方式二:
go
func NewComplexObj2(arg Arg) *ComplexObj {
return &ComplexObj{
name: arg.name,
age: arg.age,
address: arg.address,
sex: arg.sex,
}
}
当需要增加对象时,不需要修改接口,但是随着对象越来越大,arg也是越来越复杂;
方式三:
go
type Option func(*ComplexObj)
func NewComplexObj3(options ...Option) *ComplexObj {
res := &ComplexObj{}
for _, opt := range options {
opt(res)
}
return res
}
开源库中,这种方法使用特别多。当构建过程进一步复杂时,可以采用构建模式;
二、一等公民:函数
函数是一等公民,说明函数类型与其他类型具有同样的特性。可做函数参数,可做返回值,可定义变量,可以绑定方法;
可绑定方法
go
type BinaryAdder interface {
Add(int, int) int
}
type MyAdderFunc func(int, int) int
func (f MyAdderFunc) Add(x, y int) int {
return f(x, y)
}
func MyAdd(x, y int) int {
return x + y
}
// 1. 定义接口
// 2. 定义函数
// 3. 将函数显式转为接口
// 4. 使用接口调用对应的函数
func TestMyAdder(t *testing.T) {
var i BinaryAdder = MyAdderFunc(MyAdd)
fmt.Println(i.Add(5, 6))
}
上述例子中,我们将一个函数显式的转换为一个对象,这种写法在net.http
中也有被用到;
柯里化函数编程
柯里化函数,这个词,奇奇怪怪的,说到底,就是原本一个多参数的函数,固定某部分参数,并返回一个单参数的函数。这种写法具有以下特点:
- 在函数中定义函数;
- 返回函数;
- 通过闭包将函数内部的和外部连接起来。(闭包 = 函数 + 环境)
看一个例子哈;
go
func times(x, y int) int {
return x * y
}
func partialTimes(x int) func(int) int {
return func(y int) int {
return times(x, y)
}
}
// 1. 定义一个两个相乘的函数
// 2. 定一个partialTimes函数,返回一个函数
// 3. 这种写法说实在,我是没用过
func TestPartialTimes(T *testing.T) {
timesTwo := partialTimes(2)
timeThree := partialTimes(3)
fmt.Println(timesTwo(2))
fmt.Println(timeThree(2))
}
函子
函子是一个容器类型,该容器类型实现一个方法,方法接受一个函数参数,并在容器上每个元素上应用这个函数,得到的函子。
说到底就是,有一个函数,需要作用容器中的每个元素,我们有两个写法,
- 一种写法是,定义一个函数,容器作为参数传入,然后在函数中遍历所有元素,然后进行操作;
- 一种写法是,给容器定义一个方法,在给方法中对每个元素进行操作,最后返回一个新函子;
go
type Container struct {
arr []int
}
func (o *Container) Fmap(fn func(int) int) Container {
res := []int{}
for _, a := range o.arr {
res = append(res, fn(a))
}
return Container{res}
}
func TestContainer(t *testing.T) {
c := Container{arr: []int{1, 2, 3, 4}}
fn := func(a int) int {
return a + 10
}
fmt.Println(c.Fmap(fn))
fn2 := func(a int) int {
return a * 10
}
fmt.Println(c.Fmap(fn2))
}
上述代码中,使用函子的方式给每个切片中元素+10,以及*10的操作。如果不适用函子,那么需要在每个函数中执行for range
操作;
三、方法的本质
Golang中没有类,只有类型以及绑定到类型上面的方法,方法绑定到结构体上的方式有两种,一种是通过对象,一个是通过指针。这两中绑定方法他们之间的区别是什么?分别解决什么样的应用场景?(其实,在你日常开发中,大部分都使用指针类型绑定)带这些疑问,我们来看几个例子;
go
type Person struct {
age int
name string
}
func (p Person) setAge(age int) {
p.age = age
}
func (p *Person) setName(name string) {
p.name = name
}
func (p Person) getName() string {
return p.name
}
func TestPerson(t *testing.T) {
p := &Person{}
p.setAge(10)
p.setName("fanzhihao")
fmt.Println(*p) // {0 fanzhihao}
(*Person).setName(p, "zyw")
fmt.Println(p) // {0 zyw}
fmt.Println(Person.getName(*p)) // zyw
}
解释:
- 当需要改变类型属性时,接受者应该为指针,此时相当于
(*Person).setName(p, "zyw")
,此处的p
为指针; - 当不改变类型状态是,接受者为值对象,此时相当于
Person.getName(*p)
,此时里面的为值对象;
四、init函数
init
函数在go中是一类 比较特殊函数,对于此类函数需要注意这几点:
- 在同一个包内,初始化顺序为:常量 ------ 全局变量------
init
函数; - 在init函数中,我们可以做一些初始化的操作,这里报错注册某些handler,或者全局变量。比如promethemus中的
/metrics
接口,通过匿名引入的方式实现; - 包之间inti函数的执行顺序,遵循深度优先的方式,比如包A引用包B,包B引用包C,init的执行顺序为,先包C,在包B,最后包A;
- 在开发过程中,我们不应该依赖init执行顺序;
参考
- 《Go语言精进之路》