一文了解Go函数使用

引言

对于任何一门语言来说,函数是最基本的功能模块。如果让我用一句话描述函数,那一定是:根据某些输入,执行某些任务,返回某些输出的一等公民。那函数的参数具有哪些特性?返回值具有哪些特性?一等公民具有哪些特定?利用函数一等公民的特性,我们能解决什么问题?带这这些疑问,我们来一探究竟;

一、函数原型

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语言精进之路》
相关推荐
爱勇宝8 分钟前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
AskHarries24 分钟前
工具失败时怎么办:重试、回滚、人工确认和风险提示
后端·程序员
苏三说技术2 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎3 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode3 小时前
Redis 在生产项目的使用
前端·后端
用户559822481223 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode3 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战3 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha3 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn3 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端