Abel'Blog

我干了什么?究竟拿了时间换了什么?

0%

go[1]-程序结构

简介

记录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 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/

return temp;
}

/* 定义交换值函数*/
func swap(x *int, y *int) {
var temp int
temp = *x /* 保持 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
package main

import "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 指向 a 指针,a 变量的地址
* &b 指向 b 指针,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 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}

值接收器(Value Receiver)方法

在方法定义时,使用结构体的实例作为接收器。这种方式可以在方法内部对结构体的字段进行读取操作,但无法修改结构体本身。例如:

1
2
3
4
5
6
7
8
type MyStruct struct {
data int
}

func (s MyStruct) MyMethod() {
// 使用 s.data 进行读取操作
// 无法修改 s.data 或结构体本身
}

指针接收器(Pointer Receiver)方法

在方法定义时,使用结构体的指针作为接收器。这种方式可以在方法内部对结构体的字段进行读取和修改操作,可以改变结构体本身。例如:

1
2
3
4
5
6
7
8
9
10

type MyStruct struct {
data int
}

func (s *MyStruct) MyMethod() {
// 使用 s.data 进行读取或修改操作
// 可以修改 s.data 或结构体本身
}

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
// PrintStack prints to standard error the stack trace returned by runtime.Stack.
func PrintStack() {
os.Stderr.Write(Stack())
}

使用panic的时候,建议是遇到了不可恢复、导致系统无法运行的错误,否则不要使用。服务器端口占用、文件无法打开、数据库未启动。这种错误超出了程序员的控制的。程序必须停止的情况。

6.错误处理策略

  1. 将错误信息上报给调用者,并且添加额外的上下文信息;错误信息最好能描述出问题的需要的参数;
  2. 如果错误是偶发的,或者是不可预计问题导致。我们需要构造有限次数的重试机制,避免无限制的重试;
  3. 出现错误之后,程序其实已经无法执行,那就需要将错误信息输出出来,并且将程序终止;避免出现更多的问题。比如数据库已经无法写入了,机器的硬盘已经无法写入文件内容了;
  4. 错误不致命,只需要在错误级别日志中输出,然后继续执行程序;
  5. 可以忽略掉一些无关紧要的错误。
    在Go中,函数被当作第一类型(first-class values),函数像其他值一样,拥有类型,可以传送,赋值给其他变量。函数值不能做比较,所以不能当成map的key。

7.流程控制

1.for statements

查看文档:

1
2
3
4
5
6
7
8
// file:c:/go/doc/go_spec.html#For_statements

// 根据条件循环
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 main

import (
"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 main

import (
"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.初始化

1
2
func init() {
}

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详解

1

DM5bm4.png

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() {
// 创建一个map
m := map[string]string{
"c": "cat",
"a": "apple",
"b": "banana",
}

// 将map的键值对转换为切片
var keys []string
for k := range m {
keys = append(keys, k)
}

// 对切片进行排序
sort.Strings(keys)

// 遍历排序后的切片并输出map的值
for _, k := range keys {
fmt.Println(k, m[k])
}
}

vip

退出服务器的方法

1
2
3
4
5
6
7
// 等待输入回车关闭
bufio.NewReader(os.Stdin).ReadBytes('\n')

// 等待 ctrl+c 退出
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
// +build go1.8

package main

import (
"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() {
// service connections
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()

// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
// kill (no param) default send syscanll.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
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)
}
// catching ctx.Done(). timeout of 5 seconds.
select {
case <-ctx.Done():
log.Println("timeout of 5 seconds.")
}
log.Println("Server exiting")
}

Golang-Dream-web服务器例子