函数可以让我们将一个语句序列包装为一个单元,然后可以在程序的任何地方被多次调用
##5.1 函数声明
func name(parameter-list) (result-list) {
body
}
参数列表(形参列表)指定了函数的参数的名称与类型。这些参数组委局部变量,由函数的调用者来提供值。
结果列表指定了函数的返回值的类型,无返回值得函数也被称为Effect[作用]
结果列表亦可以像参数列表一样被命名,每一个命名都会声明一个零值得本地变量。
有结果列表的函数必须显式的以return语句结尾,除非函数无法运行到结尾处,或者在结尾处调用了panic异常,或者是无限for循环而无break
函数的类型有时也被称为签名[signature]。如果两个函数具有相同的参数类型序列以及相同的结果类型序列,那么这两个函数的类型/签名就是一致的。
调用者必须按照参数的声明顺序进行传值,且Go并不支持默认值以及通过参数名称传值
函数的形参以及命名的返回值,都是作为函数最外层的本地变量,被存储在相同的词法块中
Arguments【实参】是按值传递的,因此函数收到的只是值的副本,对于入参的修改不会影响到调用者。但是如果实参中包含切片、指针、Map、函数或者管道,那么会发生原处修改问题。
如果碰到一个没有函数体的函数,那么该函数是以其他语言实现的,而不是Go语言,例如:
package math
func Sin(x float64) float64 // implemented in assembly language
##5.2 递归
Go的函数支持递归调用,即它可以直接或者间接的调用自己。
##5.3 多返回值
一个函数可以返回多个返回值,类似于Scala里的元组
func MultiReturn()(name string,age int) {
return "piemon",28
}
如果返回的多值并不完全需要,那么可以使用"_"来忽略掉该值。
当函数的结果列表是被命名的,那么可以省简单使用return来返回这些多值:
func MultiReturn()(name string,age int) {
name , age = "piemon",28
return
}
这种方法也被称为“Bare return”,但是在逻辑较为复杂的情况下不要使用,因为难以理解
##5.4 Error
Go中有那么一些函数,他们总可以成功运行,比如string.Contains和strconv.FormatBoo函数,因为设计者为任何可能的输入情况都做了良好的处理,使得其在运行时几乎不会失败,除非遇到灾难性、非预测性的情况,比如运行时内存溢出。
还有一部分函数,只要参数能够满足一定的先决条件,那么也可以保证函数的成功运行,比如time.Date会将年、月、日等参数构造为time.Time结构体对象,除非传递的最后一个参数的值(即时区)是nil。这种情况下会引起panic异常.
对于剩下的绝大部分函数,永远无法确保成功执行,因为函数运行成功所依赖的因素超过了程序员所能控制的范围。
Error因此成为包API以及应用的用户接口的重要组成部分,失败只是函数运行的几种预期行为之一。
对于将失败看做是函数预期行为的函数,他会额外多返回一个结果,一般是最后一个。如果失败只有一种原因,那么这个结果一般是一个布尔值,以ok命名,例如:
value, ok := cache.Lookup(key)
if !ok {
// ...cache[key] does not exist...
}
如果失败的原因有多种,那么这个额外的结果的类型是errro类型(内置的Error类型是一种接口类型)
error可以是nil或者非nil。nil -> success ,非nil -> 失败
与其他以exception来报告失败,而不是一个普通的值来标识失败的语言不同,尽管Go也有异常机制,但是这些机制被使用在处理那些未被预料的错误,即Bug,而不是那些在健壮程序中应该被避免的常规错误。
Go使用控制流机制(如if和return)来处理异常
###5.4.1 Error处理策略
当函数返回error,调用者有责任去检查该返回值,并采取恰当的操作。
下面将5个基本的Error处理策略:
①最通用的处理方式就是向上传播Error,因此一个子例程的失败会变为调用例程的失败
func Propagate(url string)(response string, err error) {
resp, err := http.Get(url)
if err != nil {
return "",err
}
return resp.Status,err
}
如果函数体较大,导致产生错误的原因比较多,那么我们不能直接返回Error,而是需要对错误进行包装,包含执行上下文等信息。
func Propagate(url string)(response string, err error) {
resp, err := http.Get(url)
if err != nil {
return "",err
}
location,error := resp.Location();
if error != nil {
return "",fmt.Errorf("visit url %s is success,and find the location occur error, %v",url,error)
}
return location.Path,err
}
func main() {
increase := function.Increase(10);
fmt.Println(increase)
response, err := function.Propagate("http://www.baidu.com")
if err != nil {
fmt.Println("error occur ,message is",err)
} else {
fmt.Println("response location is ",response)
}
}
log:
error occur ,message is visit url http://www.baidu.com is success,and find the location occur error, http: no Location header in response
fmt.Errorf会使用fmt.Sprintf将错误信息格式化,并返回一个Error值。我们可以将被调用的函数前缀以及上下文信息添加到error信息中。当Error最终由main函数处理时候,他可以从Error中获取从根本问题到总体失败的一个清晰的因果链。
一般而言,被调用函数会再Error信息中,将调用信息(即函数信息)和参数信息(即实参)作为上下文信息,封装金Error中。而调用函数则会为其补充一些错误信息中不包含的信息进去。
②如果错误的发生是由于一些偶然的,或者是不可预测的问题,那么我们需要针对这样的操作进行重试(retry),并规定重试册数和重试间隔:
func WaitForServer(url string) error {
const timeout = 1 * time.Minute
deadline:= time.Now().Add(timeout)
for retry := 0;time.Now().Before(deadline);retry++ {
_, err := http.Get(url)
if err == nil {
return nil
}
fmt.Printf("error occur ,and the retry num is %d \n",retry)
//retry until timout
time.Sleep(time.Second << uint(retry)) // exponential back-off
}
return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}
func main(){
function.WaitForServer("https://www.123un.com");
}
log:
error occur ,and the retry num is 0
error occur ,and the retry num is 1
error occur ,and the retry num is 2
error occur ,and the retry num is 3
error occur ,and the retry num is 4
③如果程序已然不能继续运行了,那么调用者可以打印错误信息,并终止程序
然而,我们应该仅仅在main函数中这样做,在库函数中,我们应该是包装error并向上传递。除非该错误意味着程序内部的不一致,即遇到了Bug,才可以再库函数中结束程序
if err := function.WaitForServer("https://www.123un.com"); err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
os.Exit(1)
}
更便利的方式是调用log.Fatalf,如下:
if err := WaitForServer(url); err != nil {
log.Fatalf("Site is down: %v\n", err)
}
PS:源码
func Fatalf(format string, v ...interface{}) {
std.Output(2, fmt.Sprintf(format, v...))
os.Exit(1)
}
log:
error occur ,and the retry num is 0
error occur ,and the retry num is 1
error occur ,and the retry num is 2
error occur ,and the retry num is 3
error occur ,and the retry num is 4
error occur ,and the retry num is 5
2018/09/17 14:52:26 Site is down: server https://www.123un.com failed to respond after 1m0s
为了使得输出更加富有吸引力,我们可以设置log的前缀,以及是否展示时间等:
log.SetPrefix("WaitForServer...")
log.SetFlags(0)
log:
WaitForServer...Site is down: server https://www.123un.com failed to respond after 1m0s
④针对某些情况,我们仅仅需要打印出Error信息,然后继续向下运行,我们可以使用log或者fmt来打印
⑤对于某些少数情况,我们可以安全的忽略掉整个的Error,连打印都不需要
//创建临时文件夹
dir, err := ioutil.TempDir("", "scratch")
if err != nil {
return fmt.Errorf("failed to create temp dir: %v", err)
}
// ...use temp dir...
os.RemoveAll(dir) // 忽略掉Error:因为$TMPDIR会定期的被清理
Go有一套特别的编码风格用于处理Error.在失败检查之后,Failed处理通常要写在Success处理之前。如果Failed处理会导致return,那么Success处理不需要放到else子句中。
###5.4.2 End of File (EOF)
当当没有更多的输入可用时读取时候,会产生EOF Error
该Error被定义在io包中:
源码:
package io
import (
"errors"
)
...
var EOF = errors.New("EOF")
...
调用者只需要简单的比较,可以知道是否到达读取结束位置:
func Read() error {
in := bufio.NewReader(os.Stdin)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break // finished reading
}
if err != nil {
return fmt.Errorf("read failed:%v", err)
}
fmt.Println("read value is : ",r)
}
return nil;
}
##函数值
Go中将函数看做头等类值( first-class values),与其他值一样,函数也有类型,可以被赋值给变量,亦可以作为函数的入参或者返回值。
对函数值的调用,等同于对函数的调用:
func read(path string) string{
file,err := os.Open(path);
if err != nil {
return ""
}
defer file.Close()
chunks := make([]byte, 1024, 1024)
buf := make([]byte, 1024)
for {
n, e := file.Read(buf)
if e != nil && e != io.EOF{
panic("error")
}
if 0 == n {
break
}
chunks = append(chunks,buf[:n]...)
}
return string(chunks)
}
func main(){
read := read
putty := read("D:\\temp\\deadline.txt")
}
函数类型的零值是nil,调用值为nil的函数会导致panic异常
var fun func(int)int
fun(1)
log:
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x5e7f93]
函数值可以与nil比较,但是不可以彼此之间比较,亦不可以作为map的key
函数值使得我们不仅可以使用数据来参数化函数,还可以通过行为来参数化函数:
func add1(r rune) rune { return r + 1 }
func main(){
fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111"
fmt.Println(strings.Map(add1, "VMS")) // "WNT"
fmt.Println(strings.Map(add1, "Admix")) // "Benjy"
}
下面举个替换所有foo -> bar的函数:
func Expand(s string, f func(string) string) string{
fix := f("foo")
return strings.Replace(s,"foo",fix,-1)
}
func apply(string) string{
return "bar";
}
func main(){
expand := Expand("foo123foo456", apply)
fmt.Println(expand) //bar123bar456
}
##5.6 匿名函数
具名的函数仅可以在包级中声明,但是我们可以使用函数字面量【 function literal】,在任何表达式中代表一个函数。
函数字面量的预发类似于函数的声明,但是在func关键字后面没有函数名称。他是一个表达式,他的值被称为匿名函数【Anonymous Functions】
重写之前的Expand:
func Expand(s string, f func(string) string) string{
fix := f("foo")
return strings.Replace(s,"foo",fix,-1)
}
func main(){
expand = Expand("foo123foo456", func (string) string {return "func"}) //匿名函数
}
更为重要的事,通过字面量语法定义的函数,可以引用完整的词法环境(lexical environment)。即在函数中定义的内部函数,可以引用该函数的变量(即闭包)。
func Closure () func() int {
var x = 1
return func () int {
result := x * x
x ++
return result
}
}
func main(){
closure := function.Closure()
fmt.Println("1 * 1 = ",closure())//1
fmt.Println("2 * 2 = ",closure())//4
fmt.Println("3 * 3 = ",closure())//9
}
这个例子展示了,函数值不仅仅是代码,而且还有状态。匿名内部函数可以访问和更新封闭函数Closure内的变量x。
因为函数将对变量的引用隐藏起来了,所以我们将函数列为引用类型,并不允许函数之间比较
Go使用闭包技术实现函数值,我们也可以称函数值的别名为闭包。
###5.6.1 警告:捕获迭代变量
本节讲解Go的词法作用域的一个陷阱
//迭代变量的陷阱
func Iterator () {
for _,value := range [...]int{1,2,3,4,5} {
go apply(func(){
after := 2 * time.Second
time.Sleep(after)
fmt.Println("inner number print :",value)
})
fmt.Println("outer number print : ",value)
}
time.Sleep(1 * time.Minute)
}
func apply(fun func()){
fun()
}
log:
outer number print : 1
outer number print : 2
outer number print : 3
outer number print : 4
outer number print : 5
inner number print : 5
inner number print : 5
inner number print : 5
inner number print : 5
inner number print : 5
for循环语句引入了新的词法块,循环变量value在该此法块中被声明。
在该词法块中声明的函数值都共享相同的循环变量。注意:函数值所引用的是循环变量的内存地址,而不是值,因此在一定延迟后,主线程结束,value以5这个值被定格,导致所有的inner打印全部是5.
##5.7 可变参数
可以用不同数量的实参来调用的函数,称为可变参数。
eg.fmt.Printf
在声明可变参数时候,需要在最后一个参数的类型前加省略号:“…”,这标识该函数可以使用任意数量的该类型入参来调用。
func sum(vals ...int) int {
total := 0
for _, val := range vals {
total += valtotal += val
}
return total
}
在函数体内,vals变量的类型是[]int切片
在底层,调用者会分配一个数组,并将所有的参数copy进数组中,然后传递整个数组的切片给被调用的函数。
如果我们已有的数据已经是一个切片类型,那么只需要在最后一个参数后加省略号:"…"
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"
虽然在可变参数的函数内部,…int参数类型的行为看起来很像[]int,但是可变参数函数与切片函数的类型并不相同:
var fun1 = func(vals...int) int {
var total int
for _,val := range vals {
total += val
}
return total
}
var fun2 = func(vals []int) int {
var total int
for _,val := range vals {
total += val
}
return total
}
fmt.Printf("可变参数函数类型=%T",fun1)//fun 可变参数函数类型=func(...int) int
fmt.Printf("切片参数函数类型=%T",fun1)//fun 切片参数函数类型=func([]int) int
##延迟函数调用
语法:
defer 需要延迟的函数调用语句
不适用延迟函数的样例:
func DeferFunction(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return fmt.Errorf("http status not 200")
}
fmt.Println("processing http body")
resp.Body.Close()
return nil;
}
我们需要在异常出现、函数结束时,关闭打开的资源,然而使用了defer,我们可以更加优雅的实现这种逻辑
func DeferFunction(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("http status not 200")
}
fmt.Println("processing http body")
return nil;
}
从语法上讲,延迟语句是一个普通的函数或方法调用,它的前缀是关键字defer。当执行语句时,将计算函数和参数表达式,但是实际调用将被延迟,直到包含defer语句的函数完成之后,不管是正常情况下,通过执行return语句,还是失败或者中断情况下,通过恐慌【panic】。
defer可以有多个,会按照逆序执行
defer语法通常用于一对的操作,比如打开与关闭,连接与断开,加锁与释放锁。
最适合放置defer的地方就是在资源被成功获取后立即编码延迟语法
在调试复杂函数时,defer语句也可以用于“进入【on entry】”和“离开【on exit】”操作。
func BigSlowOperation(){
defer trace()()
time.Sleep(10 * time.Second)
}
func trace() func() {
now := time.Now()
fmt.Println("enter time is",now)
return func(){
after := time.Now()
fmt.Printf("exist time is %s (spend %s )",after,after.Sub(now))
}
}
func main(){
function.BigSlowOperation()
}
一个延迟的匿名函数可以访问到返回给调用者的返回值:
func DefferWithClosure(init int)(result int){
defer func(){
fmt.Println("result value is ",result)
}()
return init + 1
}
func main(){
function.DefferWithClosure(10)
}
log:
result value is 11
需要注意的是,因为延迟函数直到函数执行的最后才执行,所以循环中的延迟语句需要额外注意。有时候需要单独将循环体抽象成函数:
原始方案:
//错误的方式
func Defer(fileNames []string) error{
for _ , fileName := range fileNames {
file, e := os.Open(fileName)
if e != nil {
return e
}
defer file.Close() //可能会耗尽文件描述符,因为所有的文件关闭动作都会再函数执行完后执行
//操作file
}
return nil;
}
修正方案:
func Defer(fileNames []string) error{
for _ , fileName := range fileNames {
//使用额外的函数,来操作文件,并在该函数释放资源
if err := LoopDefer(fileName); err != nil {
return err
}
}
return nil;
}
func DisplayDefer(file *os.File) func(){
fmt.Println("enter defer ,and the fileName is",file.Name())
return func () {
fmt.Println("exist defer ,and the fileName is",file.Name())
file.Close()
}
}
##5.9 Panic
Go的类型系统在编译期间可以捕获到许多的错误,但是像数组越界、空指针等需要在运行时检查。当Go在运行时检查到有这些错误,那么他会恐慌。
在一个典型的恐慌中,正常的执行会停止,在该goroutine中的调用的所有延迟函数会被执行,然后程序崩溃,并打印日志,打印的日志包含恐慌值【panic value】,通常是排序好的错误消息;对于每个goroutine,一个堆栈跟踪【 stack trace】显示了在panic期间处于活动状态的函数调用的堆栈。这个日志消息通常有足够的信息来诊断问题的根本原因,而无需再次运行程序,因此,它应该总是包含在关于恐慌的程序的错误报告中。
并是不所有的恐慌都在运行时,我们可以直接调用内置的panic函数,它接受任何参数。当“”不可能”情况发生时,panic是最好的选择,比如:
type State int
const (
ONE State = iota
TWO
THREE
)
func (state State) toString() string {
switch state {
case ONE:
return "星期一"
case TWO:
return "星期二";
case THREE:
return "星期三";
default:
panic(fmt.Sprintf("invalid time %q", state))
}
}
func main(){
three := THREE.toString()
fmt.Println("THREE -> ",three)
const FOUR State = 4
four := FOUR.toString()
fmt.Println("FOUR -> ",four)
}
log:
THREE -> 星期三
panic: invalid time '\x04'
goroutine 1 [running]:
main.State.toString(0x4, 0x2, 0x2)
D:/go-workspace/src/enjoy/main/HttpTetser.go:74 +0x147
main.main()
D:/go-workspace/src/enjoy/main/HttpTetser.go:58 +0x1b7
断言函数的先决条件成立是很好的实践,但这很容易被过度使用。除非您能够提供更有用的错误消息或更快地检测到错误,否则断言一个条件是没有意义的,因为运行时会为您做检查。
func Reset(x *Buffer) {
if x == nil {
panic("x is nil") //没有必要的~~
}
x.elements = nil
}
尽管Go的恐慌机制与其他的语言的异常相似,但使用恐慌的情况却大不相同。因为恐慌会导致程序崩溃,它通常用于严重错误,例如程序中的逻辑不一致;勤奋的程序员认为任何崩溃都是他们代码中的缺陷的证明。在一个健壮的程序中,“预期的”错误,即由错误的输入、错误的配置或I/O失败所引起的错误,应该优雅地处理;它们最好使用error value来处理。
让我们来考虑下regexp.Compile函数,它将正则表达式编译为用于匹配的有效形式。如果以格式不正确的模式调用,它将返回一个error,但是如果调用者知道某个特定的调用不会失败,那么检查这个错误是不必要的和繁重的。在这种情况下,调用者通过恐慌来处理错误是合理的,因为它被认为是不可能的。
由于大多数正则表达式都是程序源代码中的文字,所以regexp包提供了一个 包装函数regexp.MustCompile,来做这个检查的:
package regexp
func Compile(expr string) (*Regexp, error) { /* ... */ }
func MustCompile(expr string) *Regexp {
re, err := Compile(expr)
if err != nil {
panic(err)
}
return re
}
包装函数使客户端能够方便地使用编译后的正则表达式初始化包级别的变量,如下所示:
var httpSchemeRE = regexp.MustCompile(`^https?:`) // "http:" or "https:"
当然,MustCompile函数亦不可以使用不合法的输入值
runtime.Stack可以打印出已经展开了的函数的信息(函数介绍:将调用goroutine的堆栈跟踪信息格式化到缓冲区,并返回写入到缓冲区中的字节数)。
Go的panic机制会在展开栈信息之前运行延迟函数。
##5.10 Recover
当出现恐慌【panic】时候,通常的处理方法是放弃,但并不代表总是这样,我们可能需要通过某些方式恢复运行,或者是在退出之前清理掉残局。例如一个Web服务器遇到未预期的错误时,通常不会讲客户端挂起,而是会断开连接,在开发阶段,它还需要向客户端报告错误。
如果在defer函数中调用了内置的recover函数,并且包含defer语句的函数处于恐慌状态,那么recover将结束当前的恐慌状态并返回恐慌值。处于恐慌状态的函数不会在其中断的地方继续运行,而是会正常返回。如果在其他时候调用了recover,那么它就没有效果,返回nil
不加区别地从恐慌中恢复是一种可疑的做法,因为在恐慌后,包的变量状态很少被完整的定义或记录下来。可能对数据结构的关键更新是不完整的,文件或网络连接被打开但没有关闭,或者获得了锁但没有释放。此外,如果将崩溃替换为日志文件中的一行文字,那么不加选择的恢复可能会导致bug被忽略。
在同一个包中恢复恐慌可以帮助简化对于复杂的或意外的错误的处理,但作为一般规则,您不应该尝试恢复其他包中的恐慌。公共的APIs应该将失败以error的形式报告出来。类似地,您不应该从您不维护的函数(比如调用者提供的回调函数)产生的的恐慌中恢复过来,因为您无法推断它的安全性。
例如,net/http包中的提供了一个web服务器,他会将传入的请求分发给用于所提供的处理器上。服务器调用recover,打印堆栈跟踪并继续服务,而不是让这些处理程序中发生的一个故障终止整个进程。这在实际使用中很方便,但它确实存在资源泄漏或将失败的处理器停留在一个未指定的状态(这会导致其他的问题)的风险。
基于以上原因,有选择地恢复是最安全的。换句话说,只能从原本打算需要从恐慌中恢复过来的恐慌中恢复过来,这种情况应该很少见。这种意图可以通过对panic值使用不同的、未导出的类型进行编码,并测试recovery返回的值是否具有该类型。
这种意图可以通过对恐慌值【panic value】使用不同的、非导出的类型进行编码,并测试recovery返回的值是否具有该类型。如果是这样,我们将恐慌报告为普通的error;如果没有,我们用同样的恐慌值【panic value】来调用panic函数来恢复恐慌状态。
在某些情况下是无法恢复的,例如,内存耗尽运行时的Go以致命错误终止程序。