0%

除了 spew,我们当然也可以直接选择通过 IDE 的 debug 功能来调试。

spew

go-spew 是一个帮助开发者更直观地打印变量的工具.

通过 fmt.Print 系列方法打印变量或者结构体、数组的时候,我们看到的信息其实是很不直观的,而借助 go-spew 可以非常直观地看到打印变量的信息,比如类型、结构体字段等。

安装

1
go get -u github.com/davecgh/go-spew/spew

示例

  1. 打印基础类型
1
2
3
a := 1
fmt.Println(a)
spew.Dump(a)

输出:

1
2
1
(int) 1 // 比 fmt 的输出多了类型信息
  1. 打印结构体
1
2
3
4
5
6
p := Person{
Name: "spew",
age: 23,
}
fmt.Println(p)
spew.Dump(p)

输出:

1
2
3
4
5
{spew 23}
(main.Person) { // 比 fmt 的输出多了类型、字段名、字段类型、字符串类型的长度等信息,另外还有缩进
Name: (string) (len=4) "spew",
age: (int) 23
}
  1. 打印切片
1
2
3
arr := [...]int{1, 2, 3}
fmt.Println(arr)
spew.Dump(arr)

输出:

1
2
3
4
5
6
[1 2 3]
([3]int) (len=3 cap=3) { // 比 fmt 的输出多了长度、类型、容量等信息,而且输出的格式是带缩进的
(int) 1,
(int) 2,
(int) 3
}
  1. 打印到 Writer

someWriter 是任何实现了 io.Writer 的对象。

1
spew.Fdump(someWriter, myVar1, myVar2, ...)
  1. 获取格式化的字符串

下面这个 Sdump 会返回一个字符串而不是直接输出。

1
str := spew.Sdump(myVar1, myVar2, ...)

调试 Web 应用

使用 Sdump 来获取结构体的所有详细信息。

1
2
3
4
5
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, "Hi there, %s!", r.URL.Path[1:])
fmt.Fprintf(w, "<!--\n" + html.EscapeString(spew.Sdump(w)) + "\n-->")
}

XDG 是什么

XDG基本目录规范定义了四类文件的存储路径,分别是:缓存、配置、数据和运行时文件。

这不是一个强制性的规范,但是建议这样保存我们应用产生的文件。

具体规范内容可查看: XDG 基本目录规范

用途

在我们开发一些 cli 应用或者客户端应用的时候,我们如果需要保存一些文件到用户电脑上的话,可以使用这些目录来保存。

对应各个系统下的目录路径

Linux(and BSD) Mac Windows
XDG_DATA_DIRS [/usr/local/share, /usr/share] [/Library/Application Support] %PROGRAMDATA%
XDG_DATA_HOME ~/.local/share ~/Library/Application Support %APPDATA%
XDG_CONFIG_DIRS [/etc/xdg] [/Library/Application Support] %PROGRAMDATA%
XDG_CONFIG_HOME ~/.config ~/Library/Application Support %APPDATA%
XDG_CACHE_HOME ~/.cache ~/Library/Caches %LOCALAPPDATA%
  • XDG_DATA_DIRS:数据目录
  • XDG_DATA_HOME:当前用户数据目录(上面那个是所有用户共用的)
  • XDG_CONFIG_DIRS:配置文件目录
  • XDG_CONFIG_HOME:当前用户配置目录(上面那个是所有用户共用的)
  • XDG_CACHE_HOME:缓存文件目录

使用

  1. 安装 xdg 库
1
go get -u github.com/OpenPeeDeeP/xdg
  1. 实例化
1
2
3
import "github.com/OpenPeeDeeP/xdg"

x := xdg.New("go-lib", "xdf")
  1. 使用

使用起来很简单,因为它的功能就是简单地提供几个目录而已:

1
2
3
4
5
6
// /Users/ruby 是我本地的用户主目录
fmt.Println(x.DataHome()) // /Users/ruby/Library/Application Support/go-lib/xdf
fmt.Println(x.CacheHome()) // /Users/ruby/Library/Caches/go-lib/xdf
fmt.Println(x.ConfigDirs()) // [/Library/Application Support/go-lib/xdf]
fmt.Println(x.ConfigHome()) // /Users/ruby/Library/Application Support/go-lib/xdf
fmt.Println(x.DataDirs()) // [/Library/Application Support/go-lib/xdf]

golang 的根据文件名后缀编译的机制

在这个 OpenPeeDeeP/xdg 库中,我们可以看到有不同文件都实现了 osDefaulter 类型的接口,但是使用的时候并没有报错,这是因为 golang 中我们可以给文件加上不同操作系统的后缀,这样只有当前系统有匹配的文件的时候才会编译,而其他操作系统后缀的文件就不会编译。

比如,xdg_linux.go 在我的 mac 下不会编译,而 xdg_darwin.go 这个文件会被编译,也就是说,在 mac 下,使用的具体实现是 xdg_darwin.go 里面的实现,即使在其他文件里面给同一个结构体定义了相同的方法也不影响。

什么是 slowlog

slow log 是 Redis 用来记录查询执行时间的日志系统。

查询执行时间指的是不包括客户端响应、发送回复等 IO 操作,而单单是执行一个查询命令所耗费的时间。

另外,slow log 保存在内存里面,读写速度非常快,因此你可以放心地使用它,不必担心因为开启 slow log 而损害 Redis 的速度。

设置 slowlog

slow log 的行为由两个配置参数指定,可以通过改写 redis.conf 文件或者用 CONFIG SET 命令对它进行动态的修改。

第一个选项是 slowlog-log-slower-than,它决定要对执行时间大于多少微秒(microsecond,1秒=1,000,000 微秒)的查询进行记录。

比如执行以下命令将让 slow log 记录所有查询时间大于等于 100 微秒的查询:

1
CONFIG SET slowlog-log-slower-than 100

而以下命令记录所有查询时间大于 1000 微秒的查询:

1
CONFIG SET slowlog-log-slower-than 1000

另一个选项是 slowlog-max-len,它决定 slow log 最多能保存多少条日志,slow log 本身是一个 FIFO 队列,当队列大小超过 slowlog-max-len 时,最旧的一条日志将被删除,而最新的一条日志加入到 slow log,以此类推。

以下命令让 slow log 最多保存 1000 条日志:

1
CONFIG SET slowlog-max-len 1000

使用 CONFIG GET 命令可以查询两个选项的当前值:

1
2
3
4
5
6
7
redis> CONFIG GET slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "1000"

redis> CONFIG GET slowlog-max-len
1) "slowlog-max-len"
2) "1000"

查看 slowlog

要查看 slow log,可以使用 SLOWLOG GET 或者 SLOWLOG GET number 命令,前者打印所有 slow log,最大长度取决于 slowlog-max-len 选项的值,而 SLOWLOG GET number 则只打印指定数量的日志。

最新的日志会最先被打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 为测试需要,将 slowlog-log-slower-than 设成了 10 微秒

redis> SLOWLOG GET
1) 1) (integer) 12 # 唯一性(unique)的日志标识符
2) (integer) 1324097834 # 被记录命令的执行时间点,以 UNIX 时间戳格式表示
3) (integer) 16 # 查询执行时间,以微秒为单位
4) 1) "CONFIG" # 执行的命令,以数组的形式排列
2) "GET" # 这里完整的命令是 CONFIG GET slowlog-log-slower-than
3) "slowlog-log-slower-than"

2) 1) (integer) 11
2) (integer) 1324097825
3) (integer) 42
4) 1) "CONFIG"
2) "GET"
3) "*"

3) 1) (integer) 10
2) (integer) 1324097820
3) (integer) 11
4) 1) "CONFIG"
2) "GET"
3) "slowlog-log-slower-than"

日志的唯一 id 只有在 Redis 服务器重启的时候才会重置,这样可以避免对日志的重复处理。

查看当前日志的数量

使用命令 SLOWLOG LEN 可以查看当前日志的数量。

请注意这个值和 slowlog-max-len 的区别,它们一个是当前日志的数量,一个是允许记录的最大日志的数量。

1
2
redis> SLOWLOG LEN
(integer) 14

清空日志

使用命令 SLOWLOG RESET 可以清空 slow log。

1
2
3
4
5
6
7
8
redis> SLOWLOG LEN
(integer) 14

redis> SLOWLOG RESET
OK

redis> SLOWLOG LEN
(integer) 0

在 Iris 里面,提供了一种方式可以让我们同时运行多个应用:

这里说的应用只是一个 Iris 框架实例,这个实例可以有完全不同的路由定义、中间件等。

不同端口不同应用

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
package main

import (
"log"
"net/http"
"time"

"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/recover"

"golang.org/x/sync/errgroup"
)

var g errgroup.Group

func startApp1() error {
app := iris.New().SetName("app1")
app.Use(recover.New())
app.Get("/", func(ctx iris.Context) {
ctx.JSON(iris.Map{
"code": iris.StatusOK,
"message": "Welcome server 1",
})
})

app.Build()
return app.Listen(":8080")
}

func startApp2() error {
app := iris.New().SetName("app2")
app.Use(recover.New())
app.Get("/", func(ctx iris.Context) {
ctx.JSON(iris.Map{
"code": iris.StatusOK,
"message": "Welcome server 2",
})
})

return app.Listen(":8081")
}

func main() {
g.Go(startApp1)
g.Go(startApp2)

if err := g.Wait(); err != nil {
log.Fatal(err)
}
}

在这个例子中,我们监听了两个端口 80808081,我们可以通过这两个端口来访问这两个应用。

不同域名不同应用

这个就有点类似于 nginx 了,同一个端口可以同时监听多个域名的请求:

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
package main

import (
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/apps"
"github.com/kataras/iris/v12/middleware/recover"
)

func main() {
app3 := iris.New().SetName("app3")
app3.Use(recover.New())
app3.Get("/", func(ctx iris.Context) {
ctx.WriteString("ha3")
})

app4 := iris.New().SetName("app4")
app4.Use(recover.New())
app4.Get("/", func(ctx iris.Context) {
ctx.WriteString("ha4")
})

switcher := apps.Switch(apps.Hosts{
apps.Host{
Pattern: "app3.local",
Target: app3,
},
apps.Host{
Pattern: "app4.local",
Target: app4,
},
})
switcher.Listen(":9010")
}

在这个例子中,监听了 9010 端口,但是通过 app3.localapp4.local 去访问的时候,结果是不一样的。

在 gin 里面,路由可以通过 r := gin.Default() 返回的对象来定义,这个方法会返回框架实例,而框架实例,也就是 gin.Engine 的实例,嵌套了 RouterGroup 结构体,因此可以直接通过 r 来定义路由,比如:

1
2
3
r.GET("/test", func(c *gin.Context) {
c.String(200, "Hello world!")
})

中间件

gin 里面,中间件的定义都是通过 RouterGroup 里面的 Use 方法来定义的。

中间件的定义方式

  • 使用 engine.Use() 的时候,定义的中间件是对所有请求都有效的。
  • 使用 g := r.Group("/user"); g.Use() 的时候,定义的中间件只对 /user 路由分组有效

针对路由分组的中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 不在 /user 路由分组下
r.GET("/test", func(c *gin.Context) {
c.String(200, "test...")
})

// /user 路由分组
g := r.Group("/user")
g.Use(func(c *gin.Context) {
fmt.Println("inner group")
})
g.GET("/", func(c *gin.Context) {
c.String(200, "user...")
})
g.GET("/test", func(c *gin.Context) {
c.String(200, "user test...")
})

// GET http://localhost:8085/test => test...(控制台无输出)
// GET http://localhost:8085/user => user...(同时控制台输出 “inner group”)
// GET http://localhost:8085/user/test => user test...(同时控制台输出 “inner group”)

gin 中路由的结构

gin 里面,路由实际上是一棵前缀树,树的节点保存在 Engine.trees 属性里面。

假如我们定义了如下路由,

1
2
3
r.GET("/test", func(c *gin.Context) {})
r.GET("/testa", func(c *gin.Context) {})
r.GET("/testb", func(c *gin.Context) {})

那么这棵树大概长以下这样:

  • 对于每一个 HTTP 请求方法,在 gin 的路由树里面第一层有一个节点,这个节点的类型是 tree
  • 路由树第一层节点是 tree
  • 第二层以及更高层的节点是 node

treenode 的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type methodTree struct {
method string
root *node
}

type node struct {
path string
indices string
wildChild bool
nType nodeType
priority uint32
children []*node // child nodes, at most 1 :param style node at the end of the array
handlers HandlersChain
fullPath string
}

RouterGroup 思维导图