Golang实践:函数及方法
函数
函数将一个复杂的工作切分成多个更小的模块,使得多人协作变得更加容易; 函数对它的使用者隐藏了实现细节;
函数声明
实参是按值传递的,所以函数接收到的是每个实参的副本;
修改函数的形参变量并不会影响到调用者提供的实参;
但是,如果提供的实参包含引用类型,比如指针、slice、map、函数、或者channel,那么当函数使用形参变量时就有可能会间接的修改实参变量;
递归
许多编程语言使用固定长度的函数调用栈;栈大小在64KB到2MB之间;
递归的深度会受限于固定长度的栈大小,所以当进行深度递归调用时必须谨防栈溢出;固定长度的栈甚至会造成一定的安全隐患;
RecursionError: maximum recursion depth exceeded
相比固定长的栈,GO语言的实现使用了可变长度的栈,栈的大小会随着使用而增长,可达到1GB左右的上限; 这使得我们可以安全的使用递归而不用担心溢出问题;
多返回值
一个函数如果有命名的返回值,可以忽略return语句的操作数,这称为裸返回;
错误
错误处理是包的API设计或者应用程序用户接口的重要部分,发生错误只是许预料行为中的一种而已; 这就是Go语言处理错误的方法;
如果当函数调用发生错误时返回一个附加的结果作为错误值,习惯上将错误值作为最后一个结果返回; 如果错误只有一种情况,结果通常设置为布尔类型;
更多时候,尤其对于IO操作,错误的原因可能多种多样,而调用者则需要一些详细的信息; 在这种情况下,错误的结果类型往往是error;
一般当一个函数返回一个非空错误时,它其他的结果都是未定义的而且应该忽略; 当然,也有一些函数在调用出错的情况下会返回部分有用的结果;
与其他语言不同,Go语言通过使用普通的值而非异常来报告错误; 尽管Go语言有异常机制,但是Go语言的异常只是针对程序bug导致的预料外的错误, 而不能作为常规的错误处理方法出现在程序中;
异常会陷入带有错误消息的控制流去处理它,通常会导致预期外的结果: 错误会以难以理解的栈跟踪信息报告给最终用户,这些信息大都是关于程序结构方面的而不是简单明了的错误消息; 所以GO程序使用通常的控制流机制应对错误,这种方式在错误处理逻辑方面更加小心谨慎;
错误处理策略
当一个函数调用返回一个错误时,调用者应当负责检查错误并采取合适的处理应对;
-
1.将错误传递下去
-
2.重试,超出一定的重试次数和限定时间之后再报错退出;
func WaitForServer(url string) error {
const timeout = 1 * time.Minute
deadline := time.Now().Add(timout)
for tries := 0; time.Now().Before(deadline); tries++ {
_, err := http.Head(url)
if err == nil {
return nil
}
log.Printf("server not responsing(%s); retrying ...", err)
time.Sleep(time.Second << uint(tries)) // 指数退避策略
}
return fmt.Errorf("swerver %s failed to response afater %s", url, timeout)
}
- 3.如果依旧不能顺利进行下去,调用者能输出错误然后优雅地停止程序;
但一般这样的处理应该留给主程序部分; 通常库函数应当将错误传递给调用者,除非这个错误表示一个内部一致性错误,这意味着库内部存在bug;
if err := WaitForServer(url); 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)
}
使用自定义命令的名称作为log包的前缀,并且将日期和时间略去:
log.SetPrefix("xxxx: ")
log.Println("test log;")
xxxx: 2020/07/30 16:53:16 test log;
log.SetPrefix("xxxx: ")
log.SetFlags(0)
log.Println("test log;")
xxxx: test log;
-
4.在一些错误情况下,只记录下错误信息然后程序继续运行;
-
5.在某些情况下可以直接安全地忽略掉整个日志;
函数变量
函数变量也有类型,而且可以赋值给变量或者传递或者从其他函数中返回;函数可以像其他函数一样调用;
函数的零值是nil,调用一个空的函数变量会导致宕机;
var f func(int) int
f(3)
panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x109a9ba]
goroutine 1 [running]: main.main() /Users/rainmc/GO/src/go_practice/log.go:12 +0xaa
Process finished with exit code 2
func Add1(r rune) rune {
return r + 1
}
func main() {
fmt.Println(strings.Map(Add1, "test"))
}
//uftu
匿名函数
命名函数只能在包级别的作用域进行声明,但可以使用函数字面量在任何表达式内指定函数变量;
函数字面量就像函数声明,但在func关键字后面没有函数的名称; 它是一个表达式,他的值称作匿名函数;
函数变量不仅是一段代码还可以拥有状态;里层的匿名函数能够获取和更新外层函数的局部变量; 由于这些隐藏的变量引用所以函数归类为引用类型而且函数无法进行比较;
fmt.Println(strings.Map(func(x rune) rune {return x + 1}, "1234"))
变长函数
变长函数被调用的时候可以有可变的参数个数;
func (db *DB) UpdateResource(columns ...string) (err error) {
defer func() {
if err != nil {
glog.Infof("Update resource error: %+v.", err)
}
}()
id, err = db.ORM.Update(columns...)
return
}
values := []string{“a”, “b”} UpdateResource(values…)
延迟函数调用
语法上,一个defer语句就是一个普通的函数或者方法调用,在调用之前加上关键字defer;
函数和参数表达式会在语句执行时求值,实际的调用推迟到包含的defer语句的函数结束后执行;
func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
fmt.Println("t in defer1: ", t)
}()
t += 3
return t
// === RUN TestDeferFunc1
// t in defer1: 7
// 7
}
defer语句没有限制使用次数,执行的时候以调用defer语句顺序的倒序进行;
func Deferstack() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()
panic("触发异常")
return
// === RUN TestDeferstack
// 打印后
// 打印中
// 打印前
// --- FAIL: TestDeferstack2 (0.00s)
// panic: 触发异常 [recovered]
// panic: 触发异常
}
defer语句经常使用于成对的操作,比如打开和关闭,连接和断开,加锁和解锁,即使是再复杂的控制流, 资源在任何情况下都能够正确释放;
宕机panic
Go语言的类型系统会捕获许多编译时错误,但有些错误需要在运行时进行检查(数据越界或者解引用空指针); 当Go语言运行时检测到这些错误,就会发生宕机;
Go语言的宕机机制让延迟执行的函数在栈清理之前调用;
恢复recover
退出程序通常是正确处理宕机的方式;
在一定情况下是可以进行恢复的,有时候可以在退出前清理当前混乱的情况;
如果内置的recover函数在延迟函数的内部调用,而且这个包含defer语句的函数发生宕机, recover会终止当前的宕机状态并且返回宕机的值;
defer func() {
if err := recover(); err != nil {
glog.Errorf("List resources in db error, %+v.", err)
}
}()
方法
对象就是简单的一个值或者变量,并且拥有其方法;而方法是某种特定类型的函数;
面向对象编程就是使用方法来描述每个数据结构的属性和操作,使用者不需要了解对象本身的实现;
方法声明
方法的声明和普通函数的声明类似;只是在函数名字前面多了一个参数;这个参数把这个方法绑定到这个参数对应的类型上;
指针接收者的方法
由于主调函数会复制每一个实参变量,如果函数需要更新一个变量,或者如果一个实参太大,而我们希望避免复制整个实参; 因此,我们必须使用指针来传递变量的地址
为防止混淆,不允许本身是指针的类型进行方法声明;
通过结构体内嵌组成类型
内嵌允许构成复杂的类型,该类型由许多字段构成,每个字段提供一些方法;
方法变量与表达式
方法变量
方法表达式
封装
如果变量或者方法是不能通过对象访问到的,这称作封装的变量或者方法; 封装(有时称作数据隐藏)是面向对象编程中重要的一方面;
要封装一个对象,必须使用结构体;
beego orm时区问题
dbConnDataSource := "root:xxxxx@tcp(127.0.0.1:3306)/yi_service?charset=utf8&loc=Asia%2FShanghai"
orm.RegisterDataBase("default", "mysql",dbConnDataSource, 10)