Function
函数
存在意义
- 对特定的程序进行封装。
- 对可复用程序进行封装。
- 函数是一等公民的特征,编写强大的函数接口。
函数特征
- 函数可以进行参数的传递(回调特性)。
- 函数可以作为返回值抛出(闭包特性)。
- 函数可以作为值进行变量赋值(函数值特性)。
- 函数可以实现接口(满足接口特性)。
main函数
main函数是程序的入口函数,会自执行一次。main函数就是一个普通的函数,可在任何地方调用执行。main函数无参数,不能有实参,不能有返回值。
func是函数什么的关键字,main是函数名,()是形式参数容器,{}是函数体容器:
func main() { }
作用域
- 作用域就是变量可访问的范围,即变量的访问容器。
- 在一个Go文件内,所有函数都能访问的作用域称为全局作用域。
- 每个函数内是一个局部作用域。在自己作用域中访问变量,如果自己的作用域有该变量,就不会访问外部同变量名的变量。
- 函数作用域是可以嵌套的,函数内部也可以声明函数,但是只能定义函数表达式,不能进行函数声明。
- 嵌套函数存在作用域链,内部作用域可以访问外部作用域,但反过来不行。
函数种类
- 具有函数声明。只能出现在全局作用域,函数内部不能进行具名函数声明。
func test () {}
- 函数表达式。匿名函数声明赋值给一个变量的表达式。
test := func () {}
参数
- 参数是为了让函数成为一个数据接口,接收一些值到函数内部参与函数的程序。
- 函数如果直接获取外部作用域的变量,到函数内部参与函数程序,函数在其他没有这些变量的地方,执行就会报错。
- 尽量保证定义函数的参数能更灵活、更具有可复用性的使用函数,我们总是希望函数能成为标准的输入输出(纯函数)。
- 通过参数来表明函数执行时接收的值的个数及其数据类型。
参数分类
- 形式参数,简称形参,是函数定义时对函数调用时接收的值的描述(参数占位符)。
- 实际参数,简称实参,是函数在被调用时传入的实际的值。
参数写法
Go语言中,参数原则上是一定要指明类型的,但是如果没有指定类型,会默认找到下一个最近的参数的类型作为其类型:
func getBill(
pay float32,
balance float32,
productName string
) string { }
func getBill(
pay, balance float32,
productName string
) string { }
可变参数
当参数的数量不确定时,可使用可变参数指定参数:
package main
import "fmt"
// args 本质是一个切片
func computeNumbers(args ...int) int {
res := 0
for _, value := range args {
res += value
}
return res
}
func main() {
res := computeNumbers(1, 2, 3, 4, 5)
fmt.Println(res)
}
可变参数只能在参数列表的最后出现,否则会报错。
返回值
函数执行完毕后输出的值,可在函数调用时赋值给一个变量。
如果一个函数没有返回值,赋值语句是非法的:
package main
import "fmt"
func test() {}
func main() {
// test() (no value) used as value
res := test()
}
多返回值
Go语言中的函数可直接返回多个值:
package main
import "fmt"
func computeNumbers(args ...int) (int, []int) {
res := 0
for _, value := range args {
res += value
}
return res, args
}
func main() {
res, args := computeNumbers(1, 2, 3, 4, 5)
fmt.Println(res, args)
}
返回变量名
返回值类型指定返回变量名:
package main
import "fmt"
func computeNumbers(args ...int) (sum int, originArgs []int) {
for _, value := range args {
sum += value
}
orginArgs = args
return sum, originArgs
}
func main() {
res, args := computeNumbers(1, 2, 3, 4, 5)
fmt.Println(res, args)
}
函数类型
基本形式
func () {}
带参形式
func (a int, b int) {}
带返回值形式
func () int {}
func () func () {}
func (a int, b int) func (c int, d int) {}
闭包
闭包可理解为封闭的作用域空间,主要针对函数内部的函数的一个名词,函数内部的作用域捆绑着外部作用域环境,这种现象叫做闭包。
考虑如下示例:
func test() {
fn := func() {}
}
fn的环境捆绑了test的环境,所以fn作用域可以访问test作用域内部的变量,fn不仅捆绑了test的环境,同时也捆绑着全局作用域的环境。fn可看做为一个闭包函数,无论是否访问了外部的变量。
作用域链
按照规则查找变量所形成的链。
var a = 1
func test() {
var a = 2
test1 := func() {
var a = 3
fmt.Println(a)
}
}
如上,如果在test1中使用变量a,会按照test1 -> test -> global作用域层级查找。
闭包特性
- 内部函数可以访问到外部环境的变量。
package main
import "fmt"
func test() func() {
count := 1
return func() {
fmt.Println(count)
}
}
func main() {
test1 := test()
test1()
}
- 内部函数可以访问到内部函数的参数。
package main
import "fmt"
func test(count int) func() {
return func() {
fmt.Println(count)
}
}
func main() {
test1 := test(1)
test1()
}
- 内部函数可以操作外部环境的变量和函数参数。
- 闭包函数可以传入参数进行运算。
- 闭包使外部函数变量或参数成为内部函数的私有化变量。
package main
import "fmt"
func test(count int) (func(num int) int, func(num int) int) {
increase := func(num int) int {
count += num
return count
}
decrease := func(num int) int {
count -= num
return count
}
return increase, decrease
}
func main() {
increase, decrease := test(100)
res1 := increase(1)
res2 := increase(2)
res3 := increase(3)
res4 := decrease(1)
res5 := decrease(2)
res6 := decrease(3)
// 101 103 106 105 103 100
fmt.Println(res1, res2, res3, res4, res5, res6)
}
闭包的好处
- 延长局部变量的生命周期。
- 形成类似于面向对象的变量私有化特性。
- 使外部作用域可访问到内部作用域的变量。
逃逸分析
Go编译器的优化技术,要确定一个内部变量是否要在堆内存上分配内存空间的一种分析技术。
当内部作用域访问了外部作用域的变量,这个变量就是持久化变量。
闭包会触发变量逃逸分析:
- 内部函数访问了外部变量,会分配堆内存空间给这个变量。
- 内部函数没有访问外部变量,不会分配堆内存空间给外部变量。
package main
import "fmt"
func test(b int) {
a := 1
test1 := func() {
fmt.Println(a)
}
test1()
}
func main() {
test(100)
}
如上,如果内部函数test1访问了外部变量a,那么会分配堆内存空间给变量a,否则不会。
回调函数
将函数作为参数传递,这个参数函数叫做回调函数(callback)。
func test(cb func()) {
cb()
}
回调函数把函数任务分步进行处理,函数执行期间,一部分任务交给回调函数进行处理。
把函数内部一部分特定的任务交给回调函数来完成,函数本身并不关心回调函数内部的逻辑实现。
- 函数只需把一些逻辑结果通过参数传递给回调。
- 函数只决定在函数内部什么地方执行回调函数。
- 函数本身只关心回调执行后所返回的结果。
package main
import "fmt"
func compute(
a int,
b int,
method string,
cb func(
a int,
b int,
sign string,
res int,
),
) {
_sign := ""
_res := 0
switch method {
case "PLUS":
_sign = "+"
_res = a + b
case "MINUS":
_sign = "-"
_res = a - b
case "TIMES":
_sign = "*"
_res = a * b
case "DIVISION":
_sign = "/"
_res = a / b
default:
panic("error method")
}
cb(a, b, _sign, _res)
}
func main() {
compute(1, 2, "PLUS", func(a, b int, sign string, res int) {
result := fmt.Sprintf(`
%d %s %d = %d
`, a, sign, b, res)
fmt.Println(result)
})
}
错误机制
错误通常包含语法错误、运行错误、系统操作错误等。一旦出现系统操作的错误,整个程序就会停止工作,因此捕获一些没法控制的错误是很重要的。
在Go语言中:
- 抛出异常:
panic。 - 捕获异常:
recover。 - 最终逻辑:
defer。
在Go语言中,不认为所有的错误都需要用捕获的方式来获取错误信息,每个可能抛出错误的方法都应该返回一个error。
func test() {}
func main() {
res, err := test()
}
defer
defer后面通常跟的是函数执行,即延迟函数的执行时机。defer语句都会在函数return之后或函数最后进行执行。- 多个
defer的函数倒序进行调用。 defer主要处理函数最后必须要完成的任务,如读取文件、数据库连接、锁相关等。
package main
import "fmt"
func test() int {
defer fmt.Println(11)
defer fmt.Println(22)
defer fmt.Println(33)
fmt.Println(1)
fmt.Println(2)
fmt.Println(3)
return 1
}
func main() {
/**
1
2
3
33
22
11
*/
test()
}
panic
panic用于抛出一个错误,可自定义错误信息。
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Open("./test.txt")
if err != nil {
panic("文件打开失败")
}
fmt.Println(file)
}
recover
recover用于捕获错误,防止因为错误中断程序的执行。
package main
import (
"fmt"
"os"
)
func main() {
readFile()
fmt.Println("Finished!!!")
}
func readFile() {
defer catchError()
_, err := os.Open("./test.txt")
if err != nil {
panic("文件打开失败")
}
}
func catchError() {
if err := recover(); err != nil {
fmt.Println("Catch:", err)
}
}
类型方法
定义一种自定义类型,给该类型的所有数据集成一系列的方法,使所有该类型的数据都可以直接调用这些方法,增强了方法的集成性,防止了方法的重名问题。
package main
import "fmt"
type ComputeSlice []int
func main() {
var slice ComputeSlice = []int{1, 2, 3, 4, 5}
fmt.Println(slice.Compute("PLUS"))
fmt.Println(slice.Compute("MINUS"))
}
// 给类型增加方法
func (cs ComputeSlice) Compute(method string) int {
switch method {
case "PLUS":
return Plus(cs...)
case "MINUS":
return Minus(cs...)
default:
return Plus(cs...)
}
}
func Plus(args ...int) int {
res := 0
for _, value := range args {
res += value
}
return res
}
func Minus(args ...int) int {
res := 0
for _, value := range args {
res -= value
}
return res
}
泛型
考虑如下示例:
func plusInt(a int, b int) int {
return a + b
}
func plusFloat(a float64, b float64) float64 {
return a + b
}
func plusString(a string, b string) string {
return a + b
}
上述三个函数有同样的的参数接口、同样的单值返回、同样的函数体逻辑,但却定义了三个函数来完成,这种设计及实现是否合理?
三个函数因为参数和返回值的类型不同,从而无法将它们整合成一个函数。如果要整和成一个函数,可考虑使用泛型。
函数泛型
泛型是指一个因为类型不确定而需要占位的一个标识符,这个标识符可以理解为类似于函数形参变量的东西。
泛型的作用是在函数定义时先占位一个不确定类型的位置,在函数调用的时候,通过参数类型推断来确定实际泛型对应的具体数据类型。
在Go语言中泛型是需要在函数名后面进行定义才能使用。
定义泛型
- 在函数名后面跟
[]。 - 在中括号写入泛型标识
[T]。 - 泛型定义必须有类型约束(泛型有几种可选的具体类型)。
- 如果没有类型约束,可能导致程序无效。
- 泛型标识原理上是可以写任何字母或者单词。
package main
import "fmt"
func main() {
resInt := plus(1, 2)
resFloat := plus(1.1, 2.2)
resString := plus("a", "b")
fmt.Println(resInt)
fmt.Println(resFloat)
fmt.Println(resString)
}
// A full generics definition
func plus[T int | float64 | string](a T, b T) T {
return a + b
}
在调用函数的时候,[]中可以指定具体的类型来限制参数传递的类型。如果不写中括号,则由Go通过函数的参数与返回值推断出最后结果的类型。
切片泛型
切片泛型可使用[]T的形式声明:
func forEachPrintSlice[E int | string](s []E) {
for _, value := range s {
fmt.Println(value)
}
}
any
any表示可表示任意类型:
func forEachPrintSlice[E any](s []E) {
for _, value := range s {
fmt.Println(value)
}
}
comparable
comparable可修饰泛型是否可比较:
package main
import "fmt"
func main() {
fmt.Println(equals(1, 2))
// mismatched types untyped string and untyped int
fmt.Println(equals("a", 3))
}
func equals[T comparable](a, b T) bool {
return a == b
}
自定义类型泛型
自定义类型也可使用泛型:
// type CommonSlice[T int | string] []T
// type CommonMap[K int | string, V int | string] map[K]V
package main
import "fmt"
func main() {
var intSlice CommonSlice[int] = []int{1, 2, 3, 4, 5}
fmt.Println(intSlice.Sum())
}
type CommonSlice[T int | string] []T
func (cs CommonSlice[T]) Sum() T {
var res T
for _, value := range cs {
res += value
}
return res
}
多类型泛型
泛型可指定多种类型,使用,隔开:
package main
import "fmt"
type CommonMap[K int | string, V any] map[K]V
type MyMap CommonMap[string, string]
func main() {
var myMap MyMap = MyMap{
"name": "Tom",
}
fmt.Println(myMap)
}