Go语言编程笔记5:函数
虽然整体上Go语言的函数和其它语言颇为相似,但实际上有很多其它语言中鲜见的特性,在这篇笔记中我会一一进行介绍。
定义
Go语言中的函数定义与传统语言类似,其函数签名同样由函数名、参数列表、返回值构成,只不过写法稍有区别:
package main
import "fmt"
func myFunc(message string) bool {
fmt.Println(message)
// hello
return true
}
func main() {
myFunc("hello")
}
需要说明的是,Go语言中无论是函数还是变量,都习惯于使用“驼峰”方式进行命名,且首字母是否大写取决于是否对包外或结构体外部可见。
此外,如果没有返回值,直接将返回值类型的部分空着就行,不需要像C或C++那样使用void
进行标注。
interface{}
Go语言作为强类型语言,自然需要对函数的参数类型和返回值类型进行说明,但如果我们需要传入或者返回一个任意类型的变量,需要怎么做?
在Java中,会使用Object
,因为这是所有类的基类,而基本类型也有对应的包装类,所以自然可以代表一个“任意类型”。而Go语言中是没有“类”这个概念的,自然也不存在Object
。但Go语言中有接口interface
,如果某个命名类型拥有某个接口的所有方法,我们就可以说该类型满足该接口,并且可以在两种类型之间实现转换。
详细内容会在后续介绍接口时进行说明。
而对于一个空接口interface{}
,它不具有任何方法,自然任意的命名类型都可以满足该接口,换句话说,我们就可以用空接口来“承接”任何类型的参数或者返回值:
package main
import "fmt"
func myFunc2(param interface{}) {
switch param.(type) {
case string:
fmt.Println("this is a string")
case int:
fmt.Println("this is a int")
case float64, float32:
fmt.Println("this is a float")
default:
fmt.Println("unknown type")
}
}
func main() {
myFunc2("123")
// this is a string
myFunc2(123)
// this is a int
myFunc2(123.1)
// this is a float
myFunc2(func() {})
// unknown type
}
这里需要说明的是,在Go语言中,任意命名类型(不限于结构)都是可以定义方法的,所以这里interface{}
也可以“承接”任意一个命名类型,并不仅仅局限于结构。
变长参数列表
大多数编程语言的函数都是支持变长参数列表的,Go语言也是如此:
package main
import "fmt"
func myFunc10(mesgs ...string) {
for _, message := range mesgs {
fmt.Printf("%s ", message)
}
fmt.Println()
}
func main() {
myFunc10("hello", "world", "!")
// hello world !
messages := []string{"hello", "world", "!"}
myFunc10(messages...)
// hello world !
}
需要注意的是,变长参数需要放在参数列表的最后。此外,如果要将一个序列传递给变长参数,需要使用myFunc10(messages...)
这种方式,这表示会把messages
中的元素作为变长参数列表的实参进行传递。
多返回
在传统编程语言中,一个函数都只能返回一个返回值,所以一般错误会以异常的方式出现,自然也需要用户进行相应的捕获和编写错误处理程序。某种程度上而言,正是这种语言特性决定了传统编程语言的错误处理代码的编写风格。
而Go语言在这方面相当另类,它的函数可以返回多个返回值:
package main
import "fmt"
//返回字符串的长度和首个字符
//message 字符串
//return int 字符串长度
//return rune 字符串首个字符
func myFunc3(message string) (int, rune) {
length := len(message)
runeString := []rune(message)
return length, runeString[0]
}
func main() {
length, firstRune := myFunc3("hello")
fmt.Printf("the length is %d the first rune is %s\n", length, string(firstRune))
// the length is 5 the first rune is h
length, firstRune = myFunc3("你好")
fmt.Printf("the length is %d the first rune is %s\n", length, string(firstRune))
// the length is 6 the first rune is 你
}
- 至少我没有看到过其它哪个语言也是多返回。
- 虽然Python也可以返回一个序列,可以写作
return 1,2,3
,但本质上并不相同,其返回值仍然只是一个序列,只不过在写法上看起来像是多返回。- 多返回的时候,函数签名中的多个返回值类型必须用括号包裹。
或许在一般情况下这么做没有太大意义,因为对于复杂数据的返回,我们定义一个结构体并返回同样可以实现,就像在其它语言中返回一个对象一样。这个特性最大的影响其实是错误处理,因为多返回的存在,Go语言中所有的“可预期的”错误都会以一个额外的返回值的方式进行返回,并且一般来说会放在最后一个参数的位置:
package main
import (
"fmt"
"strconv"
)
//将给定的字符串形式的数字转换为整形数字
//strNumber 字符串形式的数字
//return int 转换后的整形数字
//return error 转换时出现的错误
func myFunc4(strNumber string) (int, error) {
intNumber, err := strconv.Atoi(strNumber)
return intNumber, err
}
func callMyFunc4(strNumber string) {
intNumber, err := myFunc4(strNumber)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%s after convered is %d\n", strNumber, intNumber)
}
func main() {
callMyFunc4("123")
// 123 after convered is 123
callMyFunc4("12.5")
// strconv.Atoi: parsing "12.5": invalid syntax
callMyFunc4("abc")
// strconv.Atoi: parsing "abc": invalid syntax
}
这里调用了内置的字符串转换包strconv
实现了一个将整形字符串转换为整形数字的函数,显然,转换任意的字符串时可能出现错误,比如传入参数是一个小数的字符串,甚至根本不是数字。所以我们需要在返回一个int
作为结果的基础上,额外返回一个值作为错误信息。
Go语言中使用error
这个内置类型作为错误信息的类型,实际上这是一个接口:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
该接口的Error
方法可以返回一个字符串作为错误信息进行显示。
还有一个细节需要注意,因为error
是一个内置类型,所以在习惯上一般用err
作为实际上接收到的错误变量,比如intNumber, err := strconv.Atoi(strNumber)
。
此外,接口的0值是nil
,如果没有错误,可以直接返回一个nil
作为错误返回值。同样的,进行函数调用的时候,对于会返回错误返回值的函数,我们可以判断接收到的错误返回值是否为nil
来判断函数调用的成功还是失败。所以Go语言中一般性的错误处理代码会这样编写:
intNumber, err := myFunc4(strNumber)
if err != nil {
fmt.Println(err)
return
}
这种代码风格是在Go语言代码中大量存在的,正如前边所说,这是因为Go语言中函数可以多返回一个特性决定的。
实际上Go语言也是支持异常、捕获这种传统特性的,但其用途被仅仅局限于语言底层的错误报告和处理,并不用于一般性的应用层面的错误处理。
命名返回值
一般而言返回值是不需要进行命名的,这显得很多此一举。
但Go语言中我们是可以给返回值进行命名的,而且还可以利用这个特性编写一些看起来奇怪,但是真的有用的代码。
《奇怪可耻但有用》?哈哈哈。
裸返回
如果一个有返回值的函数,我们命名了其返回值,则可以直接使用return
语句进行返回,编译器会“自动”将相应的变量返回给调用方,这种方式称作“裸返回”(bare return):
package main
import (
"fmt"
"strconv"
)
func myFunc5(strNum string) (intNum int, err error) {
intNum, err = strconv.Atoi(strNum)
return
}
func main() {
intNum, _ := myFunc5("123")
fmt.Println(intNum)
// 123
}
这是一个用裸返回的方式改写的字符串转换函数,其中func myFunc5(strNum string) (intNum int, err error)
是定义了两个命名返回值intNum
和err
,因为这两个变量在函数签名中就定义了,所以可以在函数中直接使用:intNum, err = strconv.Atoi(strNum)
,不需要重新定义。此外,因为返回值被直接和具体变量进行了“绑定”,所以可以直接使用return
来结束函数,无需指定返回的变量,因为此时编译器可以通过命名的变量找到需要返回哪些信息。
这种使用方式和下面的代码是等价的:
package main
import (
"fmt"
"strconv"
)
func myFunc6(strNum string) (int, error) {
var intNum int
var err error
intNum, err = strconv.Atoi(strNum)
return intNum, err
}
func main() {
intNum, _ := myFunc6("123")
fmt.Println(intNum)
// 123
}
defer
defer
语句可以将一些操作“延迟”到函数退出时进行执行:
package main
import "fmt"
func myFunc7() {
defer fmt.Println("defer in myFunc is called")
fmt.Println("myFunc is returned")
return
}
func main() {
myFunc7()
// myFunc is returned
// defer in myFunc is called
}
一般来说defer
的使用场景是一些一定需要关闭的资源的处理上,比如文件、网络、多线程资源锁等:
package main
import (
"fmt"
"log"
"net/http"
)
func myFunc8(url string) {
resp, err := http.Get(url)
if err != nil {
log.Fatalln(err)
}
defer resp.Body.Close()
contentType := resp.Header.Get("Content-Type")
fmt.Println(contentType)
}
func main() {
myFunc8("http://bing.com")
// text/html; charset=utf-8
myFunc8("http://baidu.com")
// 2021/11/17 17:52:16 Get "http://baidu.com": read tcp 192.168.1.13:11584->220.181.38.148:80: wsarecv: An existing connection was forcibly closed by the remote host.
}
在这种情况下,使用defer
可以避免在业务代码很复杂,存在大量if/else
分支的时候,忘记关闭资源,或者是编写大量重复的关闭资源代码。
如果将defer
和命名返回值的特性结合起来,可以编写出一些奇奇怪怪的代码:
package main
import "fmt"
func myFunc9(a int, b int) (result int) {
defer func() {
result = result * 2
}()
return a + b
}
func main() {
fmt.Println(myFunc9(1, 2))
//6
}
在这里,return a+b
执行后,此时的返回值result
的值是3
,而defer
语句的匿名函数执行后,返回值翻倍,变成了6
,也就是说我们利用defer
在函数返回值产生后改变了返回值。
需要注意的是,这里必须将result=result*2
放在匿名函数中才行,因为defer
的运行机制是先评估表达式,然后在函数返回时进行延迟调用,如果不包含在匿名函数中,result=result*2
这个表达式会直接评估为result=0*2
也就是result=0
,这并不是我们想要的结果(实际测试中发现编译器直接报语法错误)。
函数式编程
Go语言是支持函数式编程的。
匿名函数
我们可以在Go语言中很轻松的定义、赋值、调用一个匿名函数:
package main
import "fmt"
func main() {
nonNamedFunc := func(a int, b int) int {
return a + b
}
fmt.Println(nonNamedFunc(1, 2))
//3
}
当然,也可以在上面defer
的例子中那样定义后立即执行:
package main
import "fmt"
func main() {
func() {
fmt.Println("this is a non named function")
}()
// this is a non named function
}
函数类型
既然函数可以像变量那样赋值或者作为参数传递,那么函数本身也可以看做是一种类型(type)。函数的类型实际上是由参数列表和返回值决定的,也就是说func(a int, b int){}
和func(c int, d int){}
是同一个类型的函数。
我们可以利用格式化参数%T
查看一个函数的类型:
package main
import "fmt"
func main() {
myFunc := func(a int, b int) int { return a + b }
fmt.Printf("%T", myFunc)
// func(int, int) int
}
命名函数类型
函数类型也可以进行命名,作为一种命名类型来使用:
package main
import "fmt"
type binaryOperationFunc func(float64, float64) float64
func main() {
var addFunc binaryOperationFunc
var redFunc binaryOperationFunc
if addFunc == nil {
addFunc = func(a float64, b float64) float64 { return a + b }
}
if redFunc == nil {
redFunc = func(a float64, b float64) float64 { return a - b }
}
fmt.Println(addFunc(1, 2))
// 3
fmt.Println(redFunc(1, 2))
// -1
}
在这个例子中定义了一种二元运算函数类型:type binaryOperationFunc func(float64, float64) float64
。并且可以利用这个命名类型来定义两个函数变量addFunc
和redFunc
,需要注意的是函数类型变量初始化的时候,其值是nil
。