简介 记录golang的程序结构相关知识。包括函数、包管理、基本的流程控制(for,defer,panic recover等等)。go语言中也存在函数参数的值传递,引用传递。
函数是对一系列语句打包的单元。
1.函数定义 1 2 3 func name (parameter-list) (result-list) { body }
能支持多返回值,或者无返回值。匿名函数是指的没有名字的函数。函数可以成为一个结构体字段,也能成为通道来传递。 匿名函数是一种常见的重构手段。可以将大函数分解成多个相对独立的匿名函数块,这样主干部分的调用函数将会更加简洁,做到框架和细节分离。
函数参数传递
传递类型
描述
值传递
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 func swap (x, y int ) int { var temp int temp = x x = y y = temp return temp; } func swap (x *int , y *int ) { var temp int temp = *x *x = *y *y = temp } package mainimport "fmt" func main () { var a int = 100 var b int = 200 fmt.Printf("交换前,a 的值 : %d\n" , a ) fmt.Printf("交换前,b 的值 : %d\n" , b ) swap(&a, &b) fmt.Printf("交换后,a 的值 : %d\n" , a ) fmt.Printf("交换后,b 的值 : %d\n" , b ) } func swap (x *int , y *int ) { var temp int temp = *x *x = *y *y = temp }
值接收器(Value Receiver)方法 在方法定义时,使用结构体的实例作为接收器。这种方式可以在方法内部对结构体的字段进行读取操作,但无法修改结构体本身。例如:
1 2 3 4 5 6 7 8 type MyStruct struct { data int } func (s MyStruct) MyMethod() { }
指针接收器(Pointer Receiver)方法 在方法定义时,使用结构体的指针作为接收器。这种方式可以在方法内部对结构体的字段进行读取和修改操作,可以改变结构体本身。例如:
1 2 3 4 5 6 7 8 9 10 type MyStruct struct { data int } func (s *MyStruct) MyMethod() { }
2.匿名函数 1 2 3 4 5 6 7 8 9 10 func test (x int ) func () { return func () { fmt.Println(x) } } func main () { f := test(123 ) f() }
通过匿名函数将变量123缓存再函数中。最后调用的时候,可以很方便的将123输出。
警告:捕获迭代变量Go词法作用于陷阱。即使经验丰富程序员也会在这个问题上犯错误。
1 2 3 4 5 6 7 8 9 10 11 var rmdirs []func () for _,d := range tempDirs() { dir := d os.MkdirAll(dir,0755 ) rmdirs = append (rmdirs,func () { os.RemoveAll(dir) }) } for _,rmdir := range (rmdirs{ rmdir() }
为啥要将d重新赋值成dir?原因就在循环变量中的作用域。如果直接使用d将会删除的相同的目录。这样的问题在使用defer语句也有相同问题。
3.延迟调用 defer语句是指注册一个函数,再稍后调用。一般用于资源释放,解锁,错误处理等操作。
1 2 3 4 5 6 7 8 9 func main () {f,err :=os.Open("./main.go" ) if err !=nil { fmt.Println("err" ) return } defer f.close () }
延时调用遵循FILO,堆栈式的调用。简单理解就是先进后出。 错误的用法
1 2 3 4 5 6 7 8 9 10 11 12 func main () { for I := 0 ; I<1000000 ; I++ { path := fmt.Sprintf("./log/%d.txt" ,i) f, err := os.Open(path) if err != nil { fmt.Println(err) continue } defer f.close () } }
这种情况,应该将操作文件的部分封装成小函数,避免再循环过程中,直接使用defer。 性能方面,defer方式需要花掉比较大的代价。
4.错误处理 调侃go的人,说错误处理有些返祖。”stuck in 70’s”。 提供了error的接口。err := errors.New("division by zero")
这样就能构造一个出来。有经验的程序员一般都会对错误做一些预计的处理。函数如果返回类型为error时候,都需要判断一下错误信息是否为空。
5.panic recover 类似于exception/try/catch结构化异常。函数原型如下。
1 2 func panic (v interface {}) func recover () interface {}
他们是内置函数,不是语句。panic将会终端当前流程(如:数组访问越界、空指针引用),并且触发defer的调用。在延时调用中可以通过recover捕获其中的报错。
1 2 3 4 5 6 7 func main () { defer func () { if err := recover (); err!=nil { fmt.Println(err) } } }
recover必须在panic触发之后才能捕获到错误,而且只能在延时调用函数中才能工作。不能在函数内部直接使用。
1 2 3 4 5 6 7 8 9 func catch () { log.Println("catch:" ,recover ()) } func main () { defer catch() defer log.Println(recover ()) defer recover () panic ("i am dead" ) }
调试期间可以使用 runtime/debug.PrintStack。获取完整的堆栈信息。将会输出到标准错误日志中。
1 2 3 4 func PrintStack () { os.Stderr.Write(Stack()) }
使用panic的时候,建议是遇到了不可恢复、导致系统无法运行的错误,否则不要使用。服务器端口占用、文件无法打开、数据库未启动。这种错误超出了程序员的控制的。程序必须停止的情况。
6.错误处理策略
将错误信息上报给调用者,并且添加额外的上下文信息;错误信息最好能描述出问题的需要的参数;
如果错误是偶发的,或者是不可预计问题导致。我们需要构造有限次数的重试机制,避免无限制的重试;
出现错误之后,程序其实已经无法执行,那就需要将错误信息输出出来,并且将程序终止;避免出现更多的问题。比如数据库已经无法写入了,机器的硬盘已经无法写入文件内容了;
错误不致命,只需要在错误级别日志中输出,然后继续执行程序;
可以忽略掉一些无关紧要的错误。 在Go中,函数被当作第一类型(first-class values),函数像其他值一样,拥有类型,可以传送,赋值给其他变量。函数值不能做比较,所以不能当成map的key。
7.流程控制 1.for statements 查看文档:
1 2 3 4 5 6 7 8 for a < b { a *= 2 } RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
标准目录为src、bin、pkg三个目录。
GOPATH可以指定几个目录,排在列表最前面的比当前工作空间优先级更高。go get默认会下载到第一个工作空间里面。备注:unix-like使用冒号分隔,windows使用;分割。
GOROOT指定工具链和标准库的存放位置。
2. switch statements fallthrough
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" ) func main () { a := 2 switch a { case 1 : fmt.Println("a=1" ) case 2 : fmt.Println("a=2" ) fallthrough case 3 : fmt.Println("a=3" ) case 4 : fmt.Println("a=4" ) default : fmt.Println("default" ) } }
fallthrough/[break|continue] label 这个语法可能容易被忽略,fallthrough就是让switch语句漏下去。
[break|continue] label在多重循环嵌套的时候将会使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" ) func main () { fmt.Println("1" ) Exit: for i := 0 ; i < 9 ; i++ { for j := 0 ; j < 9 ; j++ { if i+j > 15 { fmt.Print("exit" ) break Exit } } } fmt.Println("3" ) }
8.导入包 如果是系统级的包的导入
1 2 3 4 import "net/http" import osx "github.com/apple/osx/lib" import _ "github.com/qyuhen/test" import "../lib"
9.组织结构 包由一个或多个保存在同一目录下的源文件组成。 包名和目录名称并无关系,不要求保持一致,举个例子,leaf里面的go的库。目录名为go,但是里面的包使用的名称为g。不影响使用,在import的时候是写go,语句里面写g。
1 2 "github.com/name5566/leaf/go" d := g.New(10 )
同一目录下的包名需要完全一致。下列有几个特殊的包名:
main :可执行入口 all: 标准库以及 GOPATH中能找到的所有包。 std,cmd:标准库工具链 documentations:存储文档信息、无法导入。
10.权限 包内成员可以互相访问。只有大写字母的能被包外访问。当然也能通过unsafe.Pointer方式来反出数据并且调用。
11.初始化
12.内部包 将内部模块分离出来,单独包的形式维护。只能被父目录下的包访问,命名为internal目录下的包。
13.依赖管理 主要是解决三方依赖库文件冲突问题。这个概念是和内部包刚好相反,vendor目录是提供给当前工作目录,通用的目录。 注意:vendor比标准库有嫌隙更高。
14.程序的目录建议 1 2 3 4 5 6 7 8 9 10 11 12 13 14 --- | | mgr.go | -----ixxx | | -----imgr.go | | -----models | | ------mgr_impl.go
mgr.go 用于放置提供给外部调用函数;外部需要使用的类;
ixxx 目录放置模块的接口定义;
models 目录放置实现函数;
图中说明了绘制了依赖关系。这样能防止再外面循环引用的问题:
defer详解
go map[string]string 排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main () { m := map [string ]string { "c" : "cat" , "a" : "apple" , "b" : "banana" , } var keys []string for k := range m { keys = append (keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Println(k, m[k]) } }
vip 退出服务器的方法 1 2 3 4 5 6 7 bufio.NewReader(os.Stdin).ReadBytes('\n' ) sigCh := make (chan os.Signal, 1 ) signal.Notify(sigCh, syscall.SIGINT) <-sigCh
gin 服务器如何退出来;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package mainimport ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" ) func main () { router := gin.Default() router.GET("/" , func (c *gin.Context) { time.Sleep(5 * time.Second) c.String(http.StatusOK, "Welcome Gin Server" ) }) srv := &http.Server{ Addr: ":8080" , Handler: router, } go func () { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %s\n" , err) } }() quit := make (chan os.Signal) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutdown Server ..." ) ctx, cancel := context.WithTimeout(context.Background(), 5 *time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:" , err) } select { case <-ctx.Done(): log.Println("timeout of 5 seconds." ) } log.Println("Server exiting" ) }
Golang-Dream-web服务器例子