Go语言指南-6.2 函数参数与返回值

作者: Golang资深开发

6.2 函数参数与返回值

函数能够接收参数供自己使用,也可以返回零个或多个值(我们通常把返回多个值称为返回一组值)。相比与 C、C++、Java 和 C,多值返回是 Go 的一大特性,为我们判断一个函数是否正常执行(参考 [第 5.2 节])提供了方便。

我们通过 return 关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 returnpanic(参考 [第 13 章])结尾。

在函数块里面,return 之后的语句都不会执行。如果一个函数需要返回值,那么这个函数里面的每一个代码分支(code-path)都要有 return 语句。

问题 6.1:下面的函数将不会被编译,为什么呢?大家可以试着纠正过来。

   func (st *Stack) Pop() int {
    v := 0
    for ix := len(st) - 1; ix >= 0; ix-- {
    if v = st[ix]; v != 0 {
    st[ix] = 0
    return v
    }
    }
   }

函数定义时,它的形参一般是有名字的,不过我们也可以定义没有形参名的函数,只有相应的形参类型,就像这样:func f(int, int, float64)

没有参数的函数通常被称为 niladic 函数(niladic function),就像 main.main()

6.2.1 按值传递(call by value) 按引用传递(call by reference)

Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)

如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加\&符号,比如 \&variable)传递给函数,这就是按引用传递,比如 Function(&arg1),此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。(译者注:指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递。

几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。

在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。

有些函数只是完成一个任务,并没有返回值。我们仅仅是利用了这种函数的副作用,就像输出文本到终端,发送一个邮件或者是记录一个错误等。

但是绝大部分的函数还是带有返回值的。

如下,simple_function.go 里的 MultiPly3Nums 函数带有三个形参,分别是 abc,还有一个 int 类型的返回值(被注释的代码具有和未注释部分同样的功能,只是多引入了一个本地变量):

示例 6.2 [simple_function.go] $eBook-examples-chapter_6-simple_function.go

   package main

   import "fmt"

   func main() {
    fmt.Printf("Multiply 2 * 5 * 6 = %d\n", MultiPly3Nums(2, 5, 6))
    // var i1 int = MultiPly3Nums(2, 5, 6)
    // fmt.Printf("MultiPly 2 * 5 * 6 = %d\n", i1)
   }

   func MultiPly3Nums(a int, b int, c int) int {
    // var product int = a * b * c
    // return product
    return a * b * c
   }

输出显示:

   Multiply 2 * 5 * 6 = 60

如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少。

问题 6.2:

如下的两个函数调用有什么不同:

   (A) func DoSomething(a *A) {
    b = a
    }

   (B) func DoSomething(a A) {
    b = &a
    }

6.2.2 命名的返回值(named return variables)

如下,multiple_return.go 里的函数带有一个 int 参数,返回两个 int 值;其中一个函数的返回值在函数调用时就已经被赋予了一个初始零值。 getX2AndX3getX2AndX3_2 两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,需要使用 () 把它们括起来,比如 (int, int)

命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来(参考 [第 6.6 节]的 fibonacci.go 函数)。

示例 6.3 [multiple_return.go] $eBook-examples-chapter_6-multiple_return.go

   package main

   import "fmt"

   var num int = 10
   var numx2, numx3 int

   func main() {
    numx2, numx3 = getX2AndX3(num)
    PrintValues()
    numx2, numx3 = getX2AndX3_2(num)
    PrintValues()
   }

   func PrintValues() {
    fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3)
   }

   func getX2AndX3(input int) (int, int) {
    return 2 * input, 3 * input
   }

   func getX2AndX3_2(input int) (x2 int, x3 int) {
    x2 = 2 * input
    x3 = 3 * input
    // return x2, x3
    return
   }

输出结果:

   num = 10, 2x num = 20, 3x num = 30 
   num = 10, 2x num = 20, 3x num = 30 

警告:

  • return 或 return var 都是可以的。
  • 不过 return var = expression(表达式) 会引发一个编译错误:syntax error: unexpected =, expecting semicolon or newline or }

即使函数使用了命名返回值,你依旧可以无视它而返回明确的值。

任何一个非命名返回值(使用非命名返回值是很糟的编程习惯)在 return 语句里面都要明确指出包含返回值的变量或是一个可计算的值(就像上面警告所指出的那样)。 尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂。

练习 6.1 [mult_returnval.go] $eBook-exercises-chapter_6-mult_returnval.go

编写一个函数,接收两个整数,然后返回它们的和、积与差。编写两个版本,一个是非命名返回值,一个是命名返回值。

练习 6.2 [error_returnval.go] $eBook-exercises-chapter_6-error_returnval.go

编写一个名字为 MySqrt 的函数,计算一个 float64 类型浮点数的平方根,如果参数是一个负数的话将返回一个错误。编写两个版本,一个是非命名返回值,一个是命名返回值。

6.2.3 空白符(blank identifier)

空白符用来匹配一些不需要的值,然后丢弃掉,下面的 blank_identifier.go 就是很好的例子。 ThreeValues 是拥有三个返回值的不需要任何参数的函数,在下面的例子中,我们将第一个与第三个返回值赋给了 i1f1。第二个返回值赋给了空白符 _,然后自动丢弃掉。

示例 6.4 [blank_identifier.go] $eBook-examples-chapter_6-blank_identifier.go

   package main

   import "fmt"

   func main() {
    var i1 int
    var f1 float32
    i1, _, f1 = ThreeValues()
    fmt.Printf("The int: %d, the float: %f \n", i1, f1)
   }

   func ThreeValues() (int, int, float32) {
    return 5, 6, 7.5
   }

输出结果:

   The int: 5, the float: 7.500000

另外一个示例,函数接收两个参数,比较它们的大小,然后按小-大的顺序返回这两个数,示例代码为minmax.go。

示例 6.5 [minmax.go] $eBook-examples-chapter_6-minmax.go

   package main

   import "fmt"

   func main() {
    var min, max int
    min, max = MinMax(78, 65)
    fmt.Printf("Minmium is: %d, Maximum is: %d\n", min, max)
   }

   func MinMax(a int, b int) (min int, max int) {
    if a < b {
    min = a
    max = b
    min = b
    max = a
    }
    return
   }

输出结果:

   Minimum is: 65, Maximum is 78

6.2.4 改变外部变量(outside variable)

传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return 返回。如下的例子,reply 是一个指向 int 变量的指针,通过这个指针,我们在函数内修改了这个 int 变量的数值。

示例 6.6 [side_effect.go] $eBook-examples-chapter_6-side_effect.go

   package main

   import (
    "fmt"
   )

   // this function changes reply:
   func Multiply(a, b int, reply *int) {
    *reply = a * b
   }

   func main() {
    n := 0
    reply := &n
    Multiply(10, 5, reply)
    fmt.Println("Multiply:", *reply) // Multiply: 50
   }

这仅仅是个指导性的例子,当需要在函数内改变一个占用内存比较大的变量时,性能优势就更加明显了。然而,如果不小心使用的话,传递一个指针很容易引发一些不确定的事,所以,我们要十分小心那些可以改变外部变量的函数,在必要时,需要添加注释以便其他人能够更加清楚的知道函数里面到底发生了什么。

链接

文章列表

更多推荐

更多
  • Go安全-二、Go 编程语言 Go 语言规范,The Go playground,Go之旅,关键词,评论,类型,Boolean,数字的,通用编号,具体数字,无符号整数,Signed integers,浮点数,其他数字类型,一串, 在深入研究使用 GO 进行安全
    Golang资深开发

  • Go安全-三、使用文件 File basics,创建空文件,截断文件,获取文件信息,重命名文件,删除文件,打开和关闭文件,检查文件是否存在,检查读写权限,更改权限、所有权和时间戳,硬链接和符号链接,读写,复制文件,Seeking positions in a
    Golang资深开发

  • Go安全-一、Go 安全简介 About Go,Go语言设计,The History of Go,收养与社区,关于Go的常见批评,Go工具链,Go吉祥物,学习Go,为什么要用Go?,为什么使用 Go 进行安全保护?,为什么不使用 Python 呢?,为什么不使用 J
    Golang资深开发

  • Go安全-零、前言 这本书是给谁的,这本书涵盖的内容,充分利用这本书,下载示例代码文件,使用的惯例,联系,评论, 本书涵盖了 Go 编程语言,并解释了如何将其应用于网络安全行业。所涵盖的主题对于红色和蓝色团队、希望编写安全代码的开发人员以及希望保护其
    Golang资深开发

  • Go安全-十、爬虫 爬虫基础,使用 strings 包在 HTTP 响应中查找字符串,使用正则表达式查找页面中的电子邮件地址,从 HTTP 响应中提取 HTTP 头,使用 HTTP 客户端设置 Cookie,在 web 服务器上查找未列出的文件,更改请求的
    Golang资深开发

  • Go安全-六、密码学 散列,散列小文件,散列大文件,安全地存储密码,加密,加密安全伪随机数生成器CSPRNG,Symmetric encryption,AES,非对称加密,生成公钥和私钥对,对邮件进行数字签名,验证签名,TLS,生成自签名证书,创建证书签名请
    Golang资深开发

  • Go安全-七、安全 Shell(SSH) 七、安全 ShellSSH使用 GoSSH 客户端,Authentication methods,使用密码进行身份验证,使用私钥进行身份验证,验证远程主机,通过 SSH 执行命令,启动交互式 shell, 安全外壳(S
    Golang资深开发

  • Go安全-八、暴力破解 Brute forcing HTTP basic authentication,强制使用 HTML 登录表单,强制 SSH,Brute forcing database login, 蛮力攻击,也称为穷举密钥攻击,是指您尝试输入的
    Golang资深开发

  • Go安全-十三、实现漏洞利用 交叉编译,创建绑定壳,创建反向绑定壳,创建 web shell,查找可写文件,更改文件时间戳,更改文件权限,更改文件所有权, 后利用是指渗透测试的一个阶段,其中一台机器已经被利用,代码执行可用。主要任务通常是保持持久性,以便您可以
    Golang资深开发

  • Go安全-十四、总结 重述您所学的主题关于 Go 用法的更多思考,我希望你能从这本书中得到什么,了解法律、道德和技术界限,从这里到哪里去,获得帮助和学习更多,重述您所学的主题到目前为止,在本书中,我们讨论了许多关于Go和信息安全的话题。所涵盖
    Golang资深开发

  • 近期文章

    更多
    文章目录

      推荐作者

      更多