红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Go语言编程笔记
  4. 正文

Go语言编程笔记5:函数

2021年11月17日 1181点热度 0人点赞 0条评论

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)是定义了两个命名返回值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。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: Go语言 函数
最后更新:2021年11月22日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号