【零基础入门Go语言】Go语言的一等公民:函数和方法

ops/2025/1/15 4:39:18/

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

本章将会详细讲解 Go 语言的函数和方法,了解它们的声明、使用和区别。虽然在 Go 语言中函数和方法是两种概念,但他木讷的相似度非常高,只是所属的对象不同。我们先从函数开始了解。

函数

函数初探

在前面的内容中,我们已经见到了 Go 语言中一个非常重要的函数:main 函数,它是一个 Go 语言程序的入口函数。

我们来看一个 main 函数的例子:

func main() {}

它由几部分组成:

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

函数的声明

经过上一小节的介绍,相信你已经对Go语言函数的构成有了一个比较清晰的了解,现在让我们一起总结函数的声明格式,如下面的代码所示:

func funcName(params) result {body
}

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

  1. 关键字 func
  2. 函数名字 funcName
  3. 函数的参数 params,用于定义形参的变量名和类型,可以有一个参数,也可以有多个,也可以没有。
  4. result 用于定义函数的返回值,如果没有返回值,省略即可。也可以有多个返回值,需要放到哦同一个括号内。可以给返回值指定名字。
  5. body 是函数体,可以在这里写函数的代码逻辑。

现在,我们根据上面的函数声明格式,自定义一个函数:

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

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

函数中形参的定义和我们定义变量是一样的,都是变量名称在前,变量类型在后,只不过在函数中,变量名称叫做参数名称,也就是函数的形参,形参只能在该函数体内使用。函数形参的值由点用着提供,这个值也称为函数实参

在声明函数参数是,相同类型的形参可以省去多余的类型,只留一个即可。

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

像这样使用逗号分隔变量,后面统一使用 int 类型,这与变量的声明是一样的,多个相同类型的变量都可以这样声明。

多值返回

与有的编程语言不一样的是,Go语言的函数可以返回多个值,也就是多值返回。在 Go语言的标准库中,你可以看到很多这样的函数:第一个值返回函数的结果,第二个值返回函数出错的信息,这就是多值返回的经典应用。

对于 sum 函数,假设我们不允许提供的是负数,可以这样写:

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

这里需要注意的是,如果函数有多个返回值,返回值部分的类型定义需要使用小括号括起来,也就是 (int,error)。这代表函数 sum 有两个返回值,第一个是 int 类型,第二个是 error 类型,我们在函数体中使用 return 返回结果的时候,也要符合这个类型顺序。

在函数体中,可以使用 return 返回多个值,返回的多个值通过逗号分隔即可,返回多个值的类型顺序要与函数声明的返回类型顺序一致

函数有多值返回的时候,需要有多个变量接收它的值。

如果有的函数返回值不需要,可以使用**下划线(_)**丢弃它。

返回值命名

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

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

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

你可能注意到了,上面的实例中的 return 后面什么也没有。直接为命名的返回值赋值,也就等于函数有了返回值,所以可以忽略 return 的返回值。

虽然 Go语言支持函数返回值命名,但这并不是太常用,可以根据自己的需求情况,酌情选择是否对函数返回值命名。

可变参数

可变参数就是函数的参数数量是可变的,比如常见的 fmt.Println 函数。

同样一个函数,可以不传参数,可以传一个参数,可以传两个参数,还可以传多个参数,等等,这种函数就是具有可变参数的函数。

fmt.Println()
fmt.Println("hello")
fmt.Println("hello", "world")

如何定义?下面以 Println 函数为例子。

func Println(a ...interface{}) (n int, err error)

现在我们也可以定义自己的带可变参数的函数了。还是以sum函数为例,在下面的代码中,我们通过可变参数的方式,计算调用者传递的所有实参的和.

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

该函数的参数是一个可变参数,然后通过 for range 循环来计算这些参数之和。

讲到这里,相信你也看明白了,可变参数的类型其实就是切片,比如示例中 params 参数的类型是 [​]int,所以可以使用 for range 进行循环。函数有了可变参数,就可以灵活使用它了。

这里需要注意的是,如果你定义的函数中既有普通参数,又有可变参数,那么可变参数一定要放在参数列表的最末尾。

包级函数

不管是自定义的函数sum,还是我们前面多次使用过的函数 Println,都会从属于一个包(package)。sum函数属于 main包,Println函数属于 fmt包。

同一个包中的函数哪怕是私有的(函数名称首字母小写)也可以被调用。如果不同包的函数要被调用,那么函数的作用域必须是公有的,也就是函数名称的首字母要大写,比如Println

在后面的一些章节中,我会对包、作用域和模块化做详细讲解,这里可以先记住:

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

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

匿名函数和闭包

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

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

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

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

func main() {cl:=colsure()fmt.Println(cl())fmt.Println(cl())fmt.Println(cl())
}func colsure() func() int {i:=0return func() int {i++return i}
}

运行这个代码,你会看到输出打印的结果是:

1
2
3

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

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

方法

不同于函数的方法

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

// 定义一个新的类型 Age,等价于 uint
type Age uint// 定义 Age 类型的方法,接收者为 Age
func (age Age) String() {fmt.Println("the age is ", age)
}

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

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

现在方法String()就和类型Age绑定在一起了,String()是类型Age的方法。定义了接收者的方法后,就可以通过点操作符(.)来调用方法。

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

运行这段代码,可以看到如下输出:

the age is 25

接收者就是函数和方法的最大不同,此外,上面所讲到的函数具备的能力,方法也都具备。

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

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

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

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

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

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

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

这是因为Go语言编译器帮我们自动做了如下事情:如果使用一个值类型变量调用指针类型接收者的方法,Go语言编译器会自动帮我们取指针调用,以满足指针接收者的要求。

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

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

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

总结

在Go语言中,虽然存在函数和方法两个概念,但是它们基本相同,不同的是所属的对象。函数属于一个包,方法属于一个类型,所以方法也可以简单地理解为与一个类型关联的函数。

不管是函数还是方法,它们都是代码复用的第一步,也是代码职责分离的基础。掌握好函数和方法,可以让你写出职责清晰、任务明确、可复用的代码,提高开发效率、降低Bug率。

有关于匿名函数、闭包以及方法接受者等特性的详细分析,后面会有单独的文章进行分析讲解,这里只需要先记住它的特点。

这部分的内容虽然不是面试的常问考点,但是一旦面试官问了这方面的问题,你就必须要给出正确的答案,有错误的话基本上这场面试也就拜拜了,所以还是非常重要的。


http://www.ppmy.cn/ops/150198.html

相关文章

35_Lua基本语法

Lua 是一种轻量级、易于学习的脚本语言,其语法简洁明了。下面来给大家介绍下Lua的一些基本语法,我们可以创建第一个Lua程序! 1.Lua基本语法 1.1 交互式编程 Lua支持交互式编程,这使得用户可以在命令行界面(CLI)即时执行代码片段并立即看到结果。这种交互式的环境通常被…

腾讯云AI代码助手编程挑战赛-脑筋急转弯

作品简介 一个生成脑筋急转弯的网页工具&#xff0c;提升思维能力。 技术架构 使用Html语言完成图形化页面的样式&#xff0c;使用JavaScript语言来操作对应的逻辑代码。 实现过程 1、创建一个界面 2、获取数据 3、添加按钮与功能 4、程序优化调试 开发环境、开发流程 系…

面试加分项:Android Framework PMS 全面概述和知识要点

在Android面试时,懂得越多越深android framework的知识,越为自己加分。 目录 第一章:PMS 基础知识 1.1 PMS 定义与工作原理 1.2 PMS 的主要任务 1.3 PMS 与相关组件的交互 第二章:PMS 的核心功能 2.1 应用安装与卸载机制 2.2 应用更新与版本管理 2.3 组件管理 第…

鸿蒙面试 2025-01-09

鸿蒙分布式理念&#xff1f;&#xff08;个人认为理解就好&#xff09; 鸿蒙操作系统的分布式理念主要体现在其独特的“流转”能力和相关的分布式操作上。在鸿蒙系统中&#xff0c;“流转”是指涉多端的分布式操作&#xff0c;它打破了设备之间的界限&#xff0c;实现了多设备…

Springboot内置Apache Tomcat 安全漏洞(CVE-2024-50379)

背景 大家都知道我们使用Springboot开发后&#xff0c;无需再额外配置tomcat&#xff0c;因为Springboot已经帮我们内置好了tomcat。 这次在线上安全团队就扫出来了我们Springboot服务的tomcat漏洞&#xff1a; 可以看到这是2023年的洞&#xff0c;Apache Tomcat 安全漏洞(…

基于深度学习的滑块验证破解方法及模型训练过程

深度学习破解滑块验证 深度学习训练模型 目录结构 --yolov5-master--data--VOC2028--Annotations&#xff1a;标注好的xml文件--ImageSets&#xff1a;训练集--Main--test.txt--train.txt--trainval.txt--val.txt--说明.txt--JPEGImages&#xff1a;未标注的png图片--Labels…

27年《海贼王》:动漫停更,游戏加更

12月的尾声&#xff0c;今年最后一款二游《航海王壮志雄心》正式上线。 2024年&#xff0c;对于新上的二游而言&#xff0c;并不是一个友好的时间段。 由于《原神》带动二游研发浪潮&#xff0c;海量的二游项目在2023年和2024年涌现&#xff0c;导致市场彻底沦为买方市场&…

整数对最小和,暴力存储所有数组,再放容器sort一下,accumulate(s1.begin(),s2.begin()+k,0)即可。

#include <bits/stdc.h> using namespace std; //最小和问题&#xff0c;求出所有整数对求和&#xff0c;排序即可 int main() { int n1,n2; cin>>n1; int s1[n1]; for(int i0;i<n1;i) { cin>>s1[i]; } cin>>n…