Go语言编程笔记5:函数

Go语言编程笔记5:函数

image-20211108153040805

图源:wallpapercave.com

虽然整体上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)是定义了两个命名返回值intNumerr,因为这两个变量在函数签名中就定义了,所以可以在函数中直接使用: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。并且可以利用这个命名类型来定义两个函数变量addFuncredFunc,需要注意的是函数类型变量初始化的时候,其值是nil

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值