每一个程序都包含很多的函数:函数是基本的代码块。
Go是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可读性的需求,最好把 `main()` 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。
编写多个函数的主要目的是将一个需要很多行代码的复杂问题分解为一系列简单的任务(那就是函数)来解决。而且,同一个任务(函数)可以被调用多次,有助于代码重用。
(事实上,好的程序是非常注意DRY原则的,即不要重复你自己(Don't Repeat Yourself),意思是执行特定任务的代码只能在程序里面出现一次。)
当函数执行到代码块最后一行(`}` 之前)或者 `return` 语句的时候会退出,其中 `return` 语句可以带有零个或多个参数;这些参数将作为返回值(参考 [第 6.2 节](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/06.2.md))供调用者使用。简单的 `return` 语句也可以用来结束 for 死循环,或者结束一个协程(goroutine)。
Go 里面拥三种类型的函数:
* 普通的带有名字的函数
* 匿名函数或者lambda函数(参考 [第 6.8 节](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/06.8.md))
* 方法(Methods,参考 [第 10.6 节](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/10.6.md))
所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。
作为提醒,提前介绍一个语法:
这样是不正确的 Go 代码:
~~~
func g()
{
}
~~~
它必须是这样的:
~~~
func g() {
}
~~~
函数被调用的基本格式如下:
~~~
pack1.Function(arg1, arg2, …, argn)
~~~
`Function` 是 `pack1` 包里面的一个函数,括号里的是被调用函数的实参(argument):这些值被传递给被调用函数的*形参*(parameter,参考 [第 6.2 节](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/06.2.md))。函数被调用的时候,这些实参将被复制(简单而言)然后传递给被调用函数。函数一般是在其他函数里面被调用的,这个其他函数被称为调用函数(calling function)。函数能多次调用其他函数,这些被调用函数按顺序(简单而言)执行,理论上,函数调用其他函数的次数是无穷的(直到函数调用栈被耗尽)。
一个简单的函数调用其他函数的例子:
示例 6.1 [greeting.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_6/greeting.go)
~~~
package main
func main() {
println("In main before calling greeting")
greeting()
println("In main after calling greeting")
}
func greeting() {
println("In greeting: Hi!!!!!")
}
~~~
代码输出:
~~~
In main before calling greeting
In greeting: Hi!!!!!
In main after calling greeting
~~~
函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的,例如:
假设 f1 需要 3 个参数 `f1(a, b, c int)`,同时 f2 返回 3 个参数 `f2(a, b int) (int, int, int)`,就可以这样调用 f1:`f1(f2(a, b))`。
函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参与/或者不同的返回值,在 Go 里面函数重载是不被允许的。这将导致一个编译错误:
~~~
funcName redeclared in this book, previous declaration at lineno
~~~
Go 语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能;没有重载意味着只是一个简单的函数调度。所以你需要给不同的函数使用不同的名字,我们通常会根据函数的特征对函数进行命名(参考 [第 11.12.5 节](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/11.12.md))。
如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体:
~~~
func flushICache(begin, end uintptr) // implemented externally
~~~
**函数也可以以申明的方式被使用,作为一个函数类型**,就像:
~~~
type binOp func(int, int) int
~~~
在这里,不需要函数体 `{}`。
函数是一等值(first-class value):它们可以赋值给变量,就像 `add := binOp` 一样。
这个变量知道自己指向的函数的签名,所以给它赋一个具有不同签名的函数值是不可能的。
函数值(functions value)之间可以相互比较:如果它们引用的是相同的函数或者都是 nil 的话,则认为它们是相同的函数。函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数(参考 [第 6.8 节](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/06.8.md))来破除这个限制。
目前 Go 没有泛型(generic)的概念,也就是说它不支持那种支持多种类型的函数。不过在大部分情况下可以通过接口(interface),特别是空接口与类型选择(type switch,参考 [第 11.12 节](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/11.12.md))与/或者通过使用反射(reflection,参考 [第 6.8 节](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/06.8.md))来实现相似的功能。使用这些技术将导致代码更为复杂、性能更为低下,所以在非常注意性能的的场合,最好是为每一个类型单独创建一个函数,而且代码可读性更强。
- 前言
- 第一部分:学习 Go 语言
- 第1章:Go 语言的起源,发展与普及
- 1.1 起源与发展
- 1.2 语言的主要特性与发展的环境和影响因素
- 第2章:安装与运行环境
- 2.1 平台与架构
- 2.2 Go 环境变量
- 2.3 在 Linux 上安装 Go
- 2.4 在 Mac OS X 上安装 Go
- 2.5 在 Windows 上安装 Go
- 2.6 安装目录清单
- 2.7 Go 运行时(runtime)
- 2.8 Go 解释器
- 第3章:编辑器、集成开发环境与其它工具
- 3.1 Go 开发环境的基本要求
- 3.2 编辑器和集成开发环境
- 3.3 调试器
- 3.4 构建并运行 Go 程序
- 3.5 格式化代码
- 3.6 生成代码文档
- 3.7 其它工具
- 3.8 Go 性能说明
- 3.9 与其它语言进行交互
- 第二部分:语言的核心结构与技术
- 第4章:基本结构和基本数据类型
- 4.1 文件名、关键字与标识符
- 4.2 Go 程序的基本结构和要素
- 4.3 常量
- 4.4 变量
- 4.5 基本类型和运算符
- 4.6 字符串
- 4.7 strings 和 strconv 包
- 4.8 时间和日期
- 4.9 指针
- 第5章:控制结构
- 5.1 if-else 结构
- 5.2 测试多返回值函数的错误
- 5.3 switch 结构
- 5.4 for 结构
- 5.5 Break 与 continue
- 5.6 标签与 goto
- 第6章:函数(function)
- 6.1 介绍
- 6.2 函数参数与返回值
- 6.3 传递变长参数
- 6.4 defer 和追踪
- 6.5 内置函数
- 6.6 递归函数
- 6.7 将函数作为参数
- 6.8 闭包
- 6.9 应用闭包:将函数作为返回值
- 6.10 使用闭包调试
- 6.11 计算函数执行时间
- 6.12 通过内存缓存来提升性能
- 第7章:数组与切片
- 7.1 声明和初始化
- 7.2 切片
- 7.3 For-range 结构
- 7.4 切片重组(reslice)
- 7.5 切片的复制与追加
- 7.6 字符串、数组和切片的应用
- 第8章:Map
- 8.1 声明、初始化和 make
- 8.2 测试键值对是否存在及删除元素
- 8.3 for-range 的配套用法
- 8.4 map 类型的切片
- 8.5 map 的排序
- 8.6 将 map 的键值对调
- 第9章:包(package)
- 9.1 标准库概述
- 9.2 regexp 包
- 9.3 锁和 sync 包
- 9.4 精密计算和 big 包
- 9.5 自定义包和可见性
- 9.6 为自定义包使用 godoc
- 9.7 使用 go install 安装自定义包
- 9.8 自定义包的目录结构、go install 和 go test
- 9.9 通过 Git 打包和安装
- 9.10 Go 的外部包和项目
- 9.11 在 Go 程序中使用外部库
- 第10章:结构(struct)与方法(method)
- 10.1 结构体定义
- 10.2 使用工厂方法创建结构体实例
- 10.3 使用自定义包中的结构体
- 10.4 带标签的结构体
- 10.5 匿名字段和内嵌结构体
- 10.6 方法
- 10.8 垃圾回收和 SetFinalizer
- 第11章:接口(interface)与反射(reflection)
- 11.1 接口是什么
- 11.2 接口嵌套接口
- 11.3 类型断言:如何检测和转换接口变量的类型
- 11.4 类型判断:type-switch
- 11.5 测试一个值是否实现了某个接口
- 11.6 使用方法集与接口
- 11.7 第一个例子:使用 Sorter 接口排序
- 11.8 第二个例子:读和写
- 11.9 空接口
- 11.10 反射包
- 第三部分:Go 高级编程
- 第12章 读写数据
- 12.1 读取用户的输入
- 12.2 文件读写
- 12.3 文件拷贝
- 12.4 从命令行读取参数
- 12.5 用buffer读取文件
- 12.6 用切片读写文件
- 12.7 用 defer 关闭文件
- 12.8 使用接口的实际例子:fmt.Fprintf
- 12.9 Json 数据格式
- 12.10 XML 数据格式
- 12.11 用 Gob 传输数据
- 12.12 Go 中的密码学
- 第13章 错误处理与测试
- 13.1 错误处理
- 13.2 运行时异常和 panic
- 13.3 从 panic 中恢复(Recover)
- 13.4 自定义包中的错误处理和 panicking
- 13.5 一种用闭包处理错误的模式
- 13.6 启动外部命令和程序
- 13.7 Go 中的单元测试和基准测试
- 13.8 测试的具体例子
- 13.9 用(测试数据)表驱动测试
- 13.10 性能调试:分析并优化 Go 程序
- 第14章:协程(goroutine)与通道(channel)
- 14.1 并发、并行和协程
- 14.2 使用通道进行协程间通信
- 14.3 协程同步:关闭通道-对阻塞的通道进行测试
- 14.4 使用 select 切换协程
- 14.5 通道,超时和计时器(Ticker)
- 14.6 协程和恢复(recover)
- 第15章:网络、模版与网页应用
- 15.1 tcp服务器
- 15.2 一个简单的web服务器
- 15.3 访问并读取页面数据
- 15.4 写一个简单的网页应用
- 第四部分:实际应用
- 第16章:常见的陷阱与错误
- 16.1 误用短声明导致变量覆盖
- 16.2 误用字符串
- 16.3 发生错误时使用defer关闭一个文件
- 16.5 不需要将一个指向切片的指针传递给函数
- 16.6 使用指针指向接口类型
- 16.7 使用值类型时误用指针
- 16.8 误用协程和通道
- 16.9 闭包和协程的使用
- 16.10 糟糕的错误处理
- 第17章:模式
- 17.1 关于逗号ok模式
- 第18章:出于性能考虑的实用代码片段
- 18.1 字符串
- 18.2 数组和切片
- 18.3 映射
- 18.4 结构体
- 18.5 接口
- 18.6 函数
- 18.7 文件
- 18.8 协程(goroutine)与通道(channel)
- 18.9 网络和网页应用
- 18.10 其他
- 18.11 出于性能考虑的最佳实践和建议
- 附录