Go语言深入解析:函数、方法与一等函数的应用
Go语言作为一种简洁而强大的编程语言,凭借其独特的设计理念和高效的执行性能,已经在系统编程、网络服务、分布式应用等领域得到了广泛应用。在Go语言中,函数、方法与一等函数是构建高效、灵活程序的基础工具。本文将深入探讨这些核心概念,帮助读者深入理解如何通过函数与方法提升代码的可复用性、可读性,并掌握一等函数带来的灵活性。通过实际代码示例,您将了解到如何在Go语言中高效使用这些特性,从而在开发中游刃有余。
Go语言的函数和方法机制是其与其他编程语言相比的独特优势之一。本文从Go语言的函数声明、函数的多个参数与返回值、可变参数函数等基础概念讲起,逐步深入到方法、类型声明以及一等函数的高级应用。首先,文章解释了如何定义函数,如何利用函数提高代码模块化,如何通过方法赋予自定义类型行为。接着,介绍了Go语言中的一等函数特性,展示了如何将函数作为一等公民进行操作和传递,进而提高程序的灵活性与可扩展性。此外,文章通过丰富的代码示例和小测试,帮助读者加深对这些概念的理解和掌握。
函数
为什么需要函数
- 做一件事通常需要很多步骤,每个步骤可以分解为独立的函数,这些函数以后可能会复用到。
函数声明
- Go在标准库文档中列出了标准库每个包中声明的函数。
- 例如:
- rand包的Intn:func Intn(n int) int
-
它的用法:num := rand.Intn(10)
-
使用func关键字声明函数
-
在Go里,大写字母开头的函数、变量或其它标识符都会被导出,对其它包可用。
-
小写字母开头的就不行。
-
形式参数:parameter
- 实际参数:argument
函数声明 – 多个参数
- 函数的参数可以是多个:
- func Unix(sec int64, nsec int64) Time
-
调用:future := time.Unix(12622780800, 0)
-
函数声明时,如果多个形参类型相同,那么该类型只写一次即可:
- func Unix(sec int64, nsec int64) Time
- func Unix(sec, nsec int64) Time
- 这种简化是可选的。
函数声明 – 返回多个值
- Go的函数可以返回多个值:
- countdown, err := strconv.Atoi("10")
- 该函数的声明如下:
- func Atoi(s string) (i int, err error)
- 函数的多个返回值需要用括号括起来,每个返回值名字在前,类型在后。声明函数时可以把名字去掉,只保留类型:
- func Atoi(s string) (int, error)
函数声明 – 可变参数函数
- Println是一个特殊的函数,它可以接收一个、二个甚至多个参数,参数类型还可以不同:
- fmt.Println("Hello, playground")
- fmt.Println(186, "seconds")
- Println的声明是这样的:
- func Println(a ...interface{}) (n int, err error)
- … 表示函数的参数的数量是可变的。
- 参数a的类型为interface{},是一个空接口。
- … 和空接口组合到一起就可以接受任意数量、类型的参数了
小测试
- 调用函数时使用的是实参还是形参?
- 诸如Contains和contains这两个函数会有什么区别?
- 函数声明中的 … 表示什么意思?
编写函数
- 例子
package main
import "fmt"
// kelvinToCelsius converts °K to °C
func kelvinToCelsius(k float64) float64 {
k -= 273.15
return k
}
func main() {
kelvin := 294.0
celsius := kelvinToCelsius(kelvin)
fmt.Print(kelvin, "° K is ", celsius, "° C")
}
- 函数按值传递参数
- 同一个包中声明的函数在调用彼此时不需要加上包名。
小测试
- 将代码拆分为函数有什么好处?
作业题
package main
import "fmt"
// kelvinToCelsius converts °K to °C
func kelvinToCelsius(k float64) float64 {
k -= 273.15
return k
}
func main() {
kelvin := 294.0
celsius := kelvinToCelsius(kelvin)
fmt.Print(kelvin, "° K is ", celsius, "° C")
}
- 修改这段代码:
- 复用kelvinToCelsius函数,将233K转化为 ℃。
- 编写celsiusToFahrenheit函数,它可将摄氏度转化为华氏度。
- 公式为(c x 9.0/5.0) + 32.0
- 编写kelvinToFahrenheit函数,看看它能否将0K转化为约-459.67℉
方法
声明新类型
- 关键字type可以用来声明新类型:
- type celsius float64
- var temperature celsius = 20
- 虽然Celsius是一种全新的类型,但是由于它和float64具有相同的行为和表示,所以赋值操作能顺利执行。
- 例如加法等运算,也可以像float64那样使用。
- (例子)
package main
import "fmt"
func main() {
type celsius float64
const degrees = 20
var temperature celsius = degrees
temperature += 10
fmt.Println("temperature: ", temperature)
}
- 为什么要声明新类型:极大的提高代码可读性和可靠性
- 不同的类型是无法混用的
- (例子)
type celsius float64
const degrees = 20
var temperature celsius = degrees
temperature += 10
var warmUp float64 = 10
temperature += warmUp // 报错
通过方法添加行为
- 在C#、Java里,方法属于类
- 在Go里,它提供了方法,但是没提供类和对象
- Go比其他语言的方法要灵活
- 可以将方法与同包中声明的任何类型相关联,但不可以是int、float64等预声明的类型进行关联。
type celsius float64
type kelvin float64
func kelvinToCelsius(k kelvin) celsius {
return celsius(k - 273.15)
}
func (k kelvin) celsius() celsius { // celsius 是 kelvin 类型的方法
return celsius(k - 273.15)
}
- 上例中,celsius方法虽然没有参数。但它前面却有一个类型参数的接收者。
- 每个方法可以有多个参数,但只能有一个接收者。
- 在方法体中,接收者的行为和其它参数一样。
方法调用
- 变量.方法()
package main
import "fmt"
func main() {
var k kelvin = 294.0
var c celsius
c = kelvinToCelsius(k)
c = k.celsius()
fmt.Println("c: ", c)
}
type celsius float64
type kelvin float64
func kelvinToCelsius(k kelvin) celsius {
return celsius(k - 273.15)
}
func (k kelvin) celsius() celsius { // celsius 是 kelvin 类型的方法
return celsius(k - 273.15)
}
小测试
- 标识出这个方法声明中的接收者:func (f fahrenheit) celsius() celsius
作业题
- 编写一个程序:
- 它包含三种类型:celsius、fahrenheit、kelvin
- 3种温度类型之间转换的方法
一等函数
一等函数
- 在Go里,函数是头等的,它可以用在整数、字符串或其它类型能用的地方:
- 将函数赋给变量
- 将函数作为参数传递给函数
- 将函数作为函数的返回类型
将函数赋给变量
package main
import (
"fmt"
"math/rand"
)
type kelvin float64
func fakeSensor() kelvin {
return kelvin(rand.Intn(151) + 150)
}
func realSensor() kelvin {
return 0
}
func main() {
sensor := fakeSensor
fmt.Println(sensor())
sensor = realSensor
fmt.Println(sensor())
}
- 变量sensor就是一个函数,而不是函数执行的结果
- 无论sensor的值是fakeSensor还是realSensor,都可以通过sensor()来调用
- sensor这个变量的类型是函数,该函数没有参数,返回一个kelvin类型的值。
- 换一种声明形式的话:
- var sensor func() kelvin
小测试
- 如何区分“将函数本身赋给变量”和“将函数执行的结果赋给变量”这两种行为?
- 如果存在一个返回celsius温度的groundSensor函数,我们可以把它赋给上例中的sensor变量吗?
将函数传递给其它函数
- (例子)
package main
import (
"fmt"
"math/rand"
"time"
)
type kelvin float64
func measureTemperature(samples int, sensor func() kelvin) {
for i := 0; i < samples; i++ {
k := sensor()
fmt.Printf("%v° K\n", k)
time.Sleep(time.Second)
}
}
func fakeSensor() kelvin {
return kelvin(rand.Intn(151) + 150)
}
func main() {
measureTemperature(3, fakeSensor)
}
小测试
- 拥有向其它函数传递函数的能力有什么好处?
声明函数类型
- 为函数声明类型有助于精简和明确调用者的代码。
- 例如:type sensor func() kelvin
- 所以:func measureTemperature(samples int, s func() kelvin)
- 可以精简为:func measureTemperature(samples int, s sensor)
小测试
- 请使用函数类型重写一下函数的签名:
- func drawTable(rows int, getRow func(row int) (string, string))
闭包和匿名函数
- 匿名函数就是没有名字的函数,在Go里也称作函数字面值。
- 因为函数字面值需要保留外部作用域的变量引用,所以函数字面值都是闭包的。
- (例子)
例子一:
package main
import "fmt"
var f = func() {
fmt.Println("Dress up for the masquerade.")
}
func main() {
f()
}
例子二
package main
import "fmt"
func main() {
f := func(message string) {
fmt.Println(message)
}
f("Go to the party.")
}
例子三
package main
import "fmt"
func main() {
func() {
fmt.Println("Functions anonymous")
}()
}
例子四
package main
import "fmt"
type kelvin float64
// sensor function type
type sensor func() kelvin
func realSensor() kelvin {
return 0
}
func calibrate(s sensor, offset kelvin) sensor {
return func() kelvin {
return s() + offset
}
}
func main() {
sensor := calibrate(realSensor, 5)
fmt.Println(sensor())
}
- 闭包(closure)就是由于匿名函数封闭并包围作用域中的变量而得名的。
例子五
package main
import "fmt"
type kelvin float64
func main() {
var k kelvin = 294.0
sensor := func() kelvin {
return k
}
fmt.Println(sensor())
k++
fmt.Println(sensor())
}
小测试
- 匿名函数在Go中的另一个名字是什么?
- 闭包提供了哪些普通函数不具备的特性?
作业题
package main
import "fmt"
type kelvin float64
// sensor function type
type sensor func() kelvin
func realSensor() kelvin {
return 0
}
func calibrate(s sensor, offset kelvin) sensor {
return func() kelvin {
return s() + offset
}
}
func main() {
sensor := calibrate(realSensor, 5)
fmt.Println(sensor())
}
-
修改这段程序:
-
声明一个变量,并将其用作calibrate函数的offset实参,而不是使用字面值数字5。在此之后,即使修改变量,调用sensor()的结果也仍然为5。这是因为offset形参接受的是实参的副本而不是引用,也就是所谓的按值传递。
- 使用calibrate函数和今天讲的fakeSensor函数以创建新的sensor函数,然后多次调用这个新的sensor函数,看看它是否每次都会调用fakeSensor函数并产生随机的读数。
习题
温度表
-
编写一个温度转换表格程序:
-
画两个表格:
- 第一个表格有两列,第一列是摄氏度,第二列是华氏度。
- 从-40℃打印到100℃,间隔为5℃,并将摄氏度转化为华氏度。
- 第二个表格就是第一个表格的两列互换一下,从华氏度转化为摄氏度。
- 负责画线和填充值的代码都应该是可复用的。画表格和计算温度应该用不同的函数分别来实现。
- 实现一个drawTable函数,它接受一个一等函数作为参数,调用该函数就可以绘制每一行的温度。传入不同的函数就可以产生不同的输出数据。
总结
Go语言中的函数、方法与一等函数为程序员提供了强大的工具,用以编写清晰、简洁且可扩展的代码。通过本文的学习,我们理解了如何利用函数简化代码结构,如何通过方法增强类型的表现力,以及如何利用一等函数提高代码的灵活性和动态性。在实际应用中,这些特性使得Go语言在处理复杂业务逻辑和系统设计时能够保持高效和易维护。掌握这些内容将大大提升你在Go语言开发中的工作效率,并为构建高质量的软件产品奠定基础。