一文了解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语言精进之路》
相关推荐
XINGTECODE20 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶26 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺30 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
先天牛马圣体1 小时前
如何提升大型AI模型的智能水平
后端
java亮小白19971 小时前
Spring循环依赖如何解决的?
java·后端·spring
2301_811274311 小时前
大数据基于Spring Boot的化妆品推荐系统的设计与实现
大数据·spring boot·后端
草莓base2 小时前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring
Ljw...2 小时前
表的增删改查(MySQL)
数据库·后端·mysql·表的增删查改
编程重生之路2 小时前
Springboot启动异常 错误: 找不到或无法加载主类 xxx.Application异常
java·spring boot·后端