Go语言深入解析:函数、方法与一等函数的应用

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{},是一个空接口。
  • … 和空接口组合到一起就可以接受任意数量、类型的参数了

小测试

  1. 调用函数时使用的是实参还是形参?
  2. 诸如Contains和contains这两个函数会有什么区别?
  3. 函数声明中的 … 表示什么意思?

编写函数

  • 例子
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

小测试

  1. 如何区分“将函数本身赋给变量”和“将函数执行的结果赋给变量”这两种行为?
  2. 如果存在一个返回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())
}

小测试

  1. 匿名函数在Go中的另一个名字是什么?
  2. 闭包提供了哪些普通函数不具备的特性?

作业题

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函数并产生随机的读数。

习题

温度表

  • 编写一个温度转换表格程序:

  • 画两个表格:

  • 第一个表格有两列,第一列是摄氏度,第二列是华氏度。
    1. 从-40℃打印到100℃,间隔为5℃,并将摄氏度转化为华氏度。
  • 第二个表格就是第一个表格的两列互换一下,从华氏度转化为摄氏度。
  • 负责画线和填充值的代码都应该是可复用的。画表格和计算温度应该用不同的函数分别来实现。
  • 实现一个drawTable函数,它接受一个一等函数作为参数,调用该函数就可以绘制每一行的温度。传入不同的函数就可以产生不同的输出数据。

总结

Go语言中的函数、方法与一等函数为程序员提供了强大的工具,用以编写清晰、简洁且可扩展的代码。通过本文的学习,我们理解了如何利用函数简化代码结构,如何通过方法增强类型的表现力,以及如何利用一等函数提高代码的灵活性和动态性。在实际应用中,这些特性使得Go语言在处理复杂业务逻辑和系统设计时能够保持高效和易维护。掌握这些内容将大大提升你在Go语言开发中的工作效率,并为构建高质量的软件产品奠定基础。

全部评论(0)