Go基础 + 进阶

Go基础 + 进阶

学习路线

image.png

基础

http包 与 web服务

03.3. Go 如何使得 Web 工作 | 第三章. Web 基础 |《Go Web 编程》| Go 技术论坛 (learnku.com)

img

​ 图 3.9 http 包执行流程

创建 Listen Socket, 监听指定的端口,等待客户端请求到来。

Listen Socket 接受客户端的请求,得到 Client Socket, 接下来通过 Client Socket 与客户端通信。

处理客户端的请求,首先从 Client Socket 读取 HTTP 请求的协议头,如果是 POST 方法,还可能要读取客户端提交的数据,然后交给相应的 handler 处理请求,handler 处理完毕准备好客户端需要的数据,通过 Client Socket 写给客户端。

img

源码解读

03.4. Go 的 http 包详解 | 第三章. Web 基础 |《Go Web 编程》| Go 技术论坛 (learnku.com)

路由:

1
2
3
4
5
6
7
8
9
10
11
12
// ServeMux 的自定义
type ServeMux struct {
mu sync.RWMutex // 锁,由于请求涉及到并发处理,因此这里需要一个锁机制
m map[string]muxEntry // 路由规则,一个 string 对应一个 mux 实体,这里的 string 就是注册的路由表达式
hosts bool // 是否在任意的规则中带有 host 信息
}
// 下面看一下 muxEntry
type muxEntry struct {
explicit bool // 是否精确匹配
h Handler // 这个路由表达式对应哪个 handler
pattern string // 匹配字符串
}

Go 默认的路由添加是通过函数 http.Handle 和 http.HandleFunc 等来添加,底层都是调用了 DefaultServeMux.Handle(pattern string, handler Handler), 这个函数会把路由信息存储在一个 map 信息中 map[string]muxEntry,这就解决了上面说的第一点。

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
// 
type Handler interface {
ServeHTTP(ResponseWriter, *Request) // 路由实现器
}
//
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}



func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
w.Header().Set("Connection", "close")
w.WriteHeader(StatusBadRequest)
return
} // 获取handler
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
// 如上所示路由器接收到请求之后,如果是 * 那么关闭链接,不然调用 mux.Handler(r) 返回对应设置路由的处理 Handler,然后执行 h.ServeHTTP(w, r)
// 也就是调用对应路由的 handler 的 ServerHTTP 接口,那么 mux.Handler (r) 怎么处理的呢?

// 根据request 获取
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
if r.Method != "CONNECT" {
if p := cleanPath(r.URL.Path); p != r.URL.Path {
_, pattern = mux.handler(r.Host, p)
return RedirectHandler(p, StatusMovedPermanently), pattern
}
}
return mux.handler(r.Host, r.URL.Path)
}
// 根据host,path
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()

// Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}

Go 代码的执行流程#
通过对 http 包的分析之后,现在让我们来梳理一下整个的代码执行过程。

  • 首先调用 Http.HandleFunc

按顺序做了几件事:

1 调用了 DefaultServeMux 的 HandleFunc

2 调用了 DefaultServeMux 的 Handle

3 往 DefaultServeMux 的 map [string] muxEntry 中增加对应的 handler 和路由规则

  • 其次调用 http.ListenAndServe (“:9090”, nil)

按顺序做了几件事情:

1 实例化 Server

2 调用 Server 的 ListenAndServe ()

3 调用 net.Listen (“tcp”, addr) 监听端口

4 启动一个 for 循环,在循环体中 Accept 请求

5 对每个请求实例化一个 Conn,并且开启一个 goroutine 为这个请求进行服务 go c.serve ()

6 读取每个请求的内容 w, err := c.readRequest ()

7 判断 handler 是否为空,如果没有设置 handler(这个例子就没有设置 handler),handler 就设置为 DefaultServeMux

8 调用 handler 的 ServeHttp

9 在这个例子中,下面就进入到 DefaultServeMux.ServeHttp

10 根据 request 选择 handler,并且进入到这个 handler 的 ServeHTTP

mux.handler(r).ServeHTTP(w, r)
11 选择 handler:

A 判断是否有路由能满足这个 request(循环遍历 ServeMux 的 muxEntry)

B 如果有路由满足,调用这个路由 handler 的 ServeHTTP

C 如果没有路由满足,调用 NotFoundHandler 的 ServeHTTP

数据库加载原理

05.1. database/sql 接口 | 第五章. 访问数据库 |《Go Web 编程》| Go 技术论坛 (learnku.com)

模拟RESTful 路由

08.3. REST | 第八章. Web 服务 |《Go Web 编程》| Go 技术论坛 (learnku.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func adduser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// uid := r.FormValue("uid")
uid := ps.ByName("uid")
fmt.Fprintf(w, "you are add user %s", uid)
}

func main() {
router := httprouter.New()
router.GET("/", Index)
router.GET("/hello/:name", Hello)

router.GET("/user/:uid", getuser)
router.POST("/adduser/:uid", adduser)
router.DELETE("/deluser/:uid", deleteuser)
router.PUT("/moduser/:uid", modifyuser)

log.Fatal(http.ListenAndServe(":8080", router))
}

go RPC

08.4. RPC | 第八章. Web 服务 |《Go Web 编程》| Go 技术论坛 (learnku.com)

RPC 工作原理#

img

图 8.8 RPC 工作流程图

运行时,一次客户机对服务器的 RPC 调用,其内部操作大致有如下十步:

  1. 调用客户端句柄;执行传送参数
  2. 调用本地系统内核发送网络消息
  3. 消息传送到远程主机
  4. 服务器句柄得到消息并取得参数
  5. 执行远程过程
  6. 执行的过程将结果返回服务器句柄
  7. 服务器句柄返回结果,调用远程系统内核
  8. 消息传回本地主机
  9. 客户句柄由内核接收消息
  10. 客户接收句柄返回的数据

结构体嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type appsCC struct {
cc *CC
}
type CC struct {
apps *appsCC
}

这段代码在 Go 语言中定义了两个类型,分别是 `appsCC``CC`

其中,`appsCC` 是一个结构体类型,这个结构体包含了一个指向 `CC` 类型的指针。这个指针的作用是被用于在 `appsCC` 类型中访问和操作 `CC` 类型的属性和方法。

`CC` 是另一个结构体类型,这个结构体中包含了一个指向 `appsCC` 类型的指针。这个指针的作用是被用于在 `CC` 类型中访问和操作 `appsCC` 类型的属性和方法。

这种定义可以被视为是一种嵌套结构体的方式,通过指针的方式将两个结构体类型相互引用。这样的设计有助于实现模块化和代码复用,使得程序更易于维护和扩展。

需要注意的是,这种定义并不会发生循环嵌套的问题,因为 `apps` 属性和 `cc` 属性的类型是不同的结构体类型,它们互相引用不会形成死循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在 Go 语言中,内嵌(嵌入)是指在一个结构体类型中嵌入另一个结构体类型。

内嵌指针和内嵌结构体的区别在于:

1. 内嵌指针:在一个结构体类型中嵌入了另一个结构体类型指针,可以通过指针访问内嵌结构体的属性和方法,这种方式可以降低内存占用和提高程序效率。内嵌指针与外部结构体分别分配内存,外部结构体包含了内部结构体的地址,不会将整个内部结构体复制到外部结构体中。

下面是内嵌指针的示例代码:
type A struct {
a int
}
type B struct {
*A
b int
}

b := &B{&A{a: 1}, 2} // 创建 B 的实例并初始化 a 和 b
fmt.Println(b.a) // 通过指针访问内嵌结构体的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14

2. 内嵌结构体:在一个结构体类型中嵌入了另一个结构体类型,可以通过外层结构体访问内层结构体的属性和方法。内嵌结构体与外部结构体在同一个结构体中分配内存,这意味着内嵌结构体不能独立存在,必须随着外部结构体一起创建和销毁。

下面是内嵌结构体的示例代码:
type A struct {
a int
}
type B struct {
A
b int
}

b := B{A: A{a: 1}, b: 2} // 创建 B 的实例并初始化 A 的属性和 b
fmt.Println(b.A.a) // 通过外层结构体访问内层结构体的属性

需要根据具体情况选择内嵌指针或内嵌结构体,以达到最优的性能和易用性。

元标记

在Go语言中,结构体字段后面的反引号中可以添加元标记(tags)来指定该字段的元数据。这些标记通常用于提供编译时检查或代码生成的元信息。

下面是一些常见的元标记及其含义:

  • omitempty:隐式忽略空值字段(如 nil0false""),以减小生成的JSON、XML或Gob等的输出文件大小。
  • stringintfloat等:强制将该字段解析为指定的类型。
  • json:覆盖JSON输出中的字段名,如:json:"my_field" 表示在输出中使用 my_field 而非字段的原始名称。
  • xml:同上,但是用于XML输出。
  • msgpack:同上,但是用于MessagePack输出。
  • bson:同上,但是用于BSON输出。
  • yaml:同上,但是用于YAML输出。
  • db:针对数据库模型,覆盖表字段名的名称或类型。
  • validate:提供快速的参数验证标准,如validate:"required,min=10,max=100" 强制验证字符串的长度,以及值的范围。

这些元标记一般集成在各种库和框架中,以简化开发人员的开发。例如,在 Gin、Echo、Beego 和 Revel 等框架中,都有基于元标记的验证和绑定机制,可以方便地进行参数验证和提供更好的表单,JSON 和 XML 绑定。

标准依赖库

依赖使用介绍:Standard library - Go Packages

源码地址:Sub-repositories - Go Packages

导入包和 导入init方法 (‘’_’’)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import (
"encoding/json"
"strconv"
)

import (
_ "encoding/json"
_ "strconv"
)
二者的区别

这两种 `import` 的方式都是用于在 Golang 中导入外部包或库,但两者的使用场景和含义有所不同。

第一种方式是直接导入 `"encoding/json"``"strconv"` 这两个包,这意味着在代码中可以直接使用这两个包中的函数和类型,比如 `json.Marshal``strconv.Atoi`

第二种方式则使用了 `_` 符号,表示不直接使用这两个包中的任何函数和类型,只是为了执行包中 `init()` 函数。在 Go 中,每个包都可以包含一个 `init()` 函数,这个函数在包加载时会自动执行,用于初始化相关的变量和设置。如果一个包中有一些全局变量或设置需要在程序运行时自动执行,那么可以将它们放在 `init()` 中。

在使用第二种方式时,虽然没有在代码中直接使用这两个包的函数和变量,但是它们的 `init()` 函数还是会被自动执行的。另外,这种方式常用在需要加载某些插件或库时,可以保证插件或库的 `init()` 函数被执行。

需要注意的是,第二种方式不会污染包作用域。也就是说,虽然导入了包,但是包中的函数和变量不能在代码中直接使用,也没有和包的名称进行关联。如果在代码中尝试使用 `"encoding/json"` 或者 `"strconv"` 的函数或变量,编译器会报错,因为它们未被定义。

go mod 导入依赖

使用go mod进行依赖管理时,常用的命令包括:

  1. go mod init:初始化一个新的go.mod文件来管理项目依赖。
  2. go mod tidy:根据go.mod文件中记录的依赖关系,自动添加或删除项目中使用但未在go.mod文件中声明的依赖包,并且保持go.mod和go.sum文件的同步。
  3. go mod vendor:将依赖包复制到项目的vendor目录中,以便于在没有网络连接的情况下构建项目。
  4. go mod download:下载go.mod文件中记录的所有依赖包到本地缓存中。
  5. go mod verify:校验依赖包的完整性,检查依赖包的hash值是否匹配go.sum文件中的记录。
  6. go mod graph:打印当前项目的模块依赖图。
  7. go mod why:打印指定的包是为何需要被包含在项目中的原因。
  8. go mod edit:修改go.mod文件中的依赖关系。
  9. go mod replace:替换go.mod文件中的特定依赖包。
  10. go list -m all:列出所有导入路径及其对应的版本。

这些命令可以通过在命令行中输入”go mod”加上相应的命令来执行。

断言表达式

interface.(*type xx struct) 是一个有效的类型断言表达式,其中 xx 是你想要断言的具体类型的名称。

在 Go 语言中,使用类型断言表达式 value.(Type) 来将接口类型的值 value 转换为特定类型 Type

具体类型名称应该替换为你想要断言的实际类型的名称。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Person struct {
Name string
Age int
}

var x interface{} = Person{"John", 30}

p, ok := x.(Person)
if ok {
fmt.Println("x is a Person:", p.Name, p.Age)
} else {
fmt.Println("x is not a Person")
}

在上面的示例中,我们将 x 断言为 Person 类型,并检查断言结果以确定是否成功。如果断言成功,我们可以使用断言后的变量 p 来访问 Person 结构体的字段;如果断言失败,我们可以据此处理。

请确保将 xx 替换为你想要断言的具体类型的名称,以使类型断言有效。

进阶

进阶学习方式

如果想要深入了解 Go 语言的底层逻辑,可以从以下几个方向入手:

  1. Go 语言编译器:了解 Go 语言编译器和编译器优化技术,理解生成的汇编代码和可执行文件的结构。

  2. Go 语言运行时系统:了解 Go 语言的内存管理、垃圾回收、调度器、并发机制、信号处理和系统调用等底层机制。

  3. Go 语言标准库:了解 Go 标准库的设计思想和底层实现。

以下是一些推荐的阅读资料:

  1. 《Go语言编程》:一本从基础到进阶都很全面的 Go 语言教材,涉及 Go 语言的基础知识、网络编程、进程和线程、性能分析和调优等方面。

  2. 《Go语言高级编程》:一本涉及 Go 语言高级主题的教材,包括 Go 语言运行时系统、内存管理、垃圾回收、网络编程和并发编程等。

  3. 《Go源码剖析》:一本介绍 Go 语言源码的书籍,通过深入阅读和分析 Go 语言的源代码,让读者深入了解 Go 语言底层机制和实现。

  4. Go 官方文档:官方文档包括 Go 语言规范、Go 语言标准库文档、Go 语言编译器和运行时系统文档等,是了解 Go 语言底层机制的重要途径。

  5. Go官方代码库:Go 语言的开源代码库提供了大量的实现细节和技术细节,可以通过阅读代码库来了解 Go 语言底层实现。

  6. Go源码分析系列博客:由国内 Go 社区的大佬们写就的针对 Go 源代码的解读分析系列博客,可以更深入地了解 Go 的设计思想和底层实现。

  7. Go 官方博客:官方博客提供了关于 Go 语言开发和技术方面的信息,它涵盖了从新手到专业人士的各种主题。

需要注意的是,Go 语言有其独特的设计思想和底层实现,初学者可能需要逐步了解相关概念和内部实现,尤其是对于底层系统设计和并发机制。因此建议从基础到进阶,一步一步深入学习,注重理解底层原理,通过实践编写代码并进行调试。

init 和 main

在 Go 中,init() 函数的执行顺序与包的导入顺序有关。具体来说,每个文件中的 init() 函数会在文件被编译时被执行,而导入该文件的包的 init() 函数则会在导入包时被执行,按照导入的顺序依次执行。因此,导入该文件的包的 init() 函数的执行顺序是在每个文件内的 init() 函数的前面。

在程序执行时,与包被加载的时间无关,init() 函数会在程序启动时自动执行。具体来说,在程序的 main() 函数执行前,Go 会自动执行所有包中的 init() 函数,包括第三方依赖的包。由于 Go 的包是惰性加载的,在编写程序时可能会导入一些不必要的包,因此需要根据实际情况减少导入不必要的包,以提高程序的启动速度。

需要注意的是,如果一个包被多次导入,那么其中的 init() 函数也会被多次执行。为了避免这种情况,可以使用 sync.Once 等方式来保证 init() 函数只被执行一次。

启动流程

在 Go 中,如果一个包被多次导入,则该包的 init() 函数会被多次执行。这个含义是指在一个程序中,如果多次导入同一个包,该包的 init() 函数会被多次调用,而不是只调用一次。这种情况可能会导致不必要的性能开销,因此应该尽可能避免多次导入同一个包的情况。

Go 的执行方式包含三个阶段:编译、链接和运行。在编译阶段,Go 会将代码编译为目标平台可执行的二进制文件。在链接阶段,Go 会将编译得到的二进制文件与所有的依赖库链接,生成最终的可执行文件。在运行阶段,用户通过执行生成的可执行文件来运行程序。

go build 命令用于编译 Go 程序,可以将指定的 Go 源代码文件编译为可执行文件。该命令会执行编译和链接两个阶段,但不会执行最终的运行阶段。编译后的结果是一个二进制文件,而不是直接执行程序。例如,执行以下命令编译程序:

1
go build main.go

该命令会将 main.go 编译为可执行文件 main,可以通过以下命令来运行:

1
./main

go run 命令用于编译并执行 Go 程序,它执行的是编译、链接和最终的运行三个阶段。例如,执行以下命令编译并执行程序:

1
go run main.go

go install 命令用于编译 Go 程序,并安装生成的可执行文件到 $GOPATH/bin 目录中。这个命令会在编译、链接和最终的运行三个阶段都执行,并将编译生成的可执行文件安装到系统中。如果在 $GOPATH/bin$PATH 环境变量中配置了该目录,那么就可以在任何地方通过命令行来执行该程序。

在使用以上命令时,需要注意的是,在执行 go buildgo install 等命令时,如果多次导入同一个包,会导致该包的 init() 函数被多次调用,增加了编译、链接和运行的时间。因此,在编写程序时应尽量避免多次导入同一个包。可以使用 go mod 命令管理依赖关系,减少包的导入次数。

同时,应该注意在安装和执行 Go 程序时,确保系统环境变量 $GOPATH$PATH 都正确配置,以确保程序可以被正确地编译、安装和执行。

Go 本地缓存

golang本地缓存(bigcache/freecache/fastcache等)选型对比及原理总结 - 知乎 (zhihu.com)


Go基础 + 进阶
http://example.com/2023/06/01/Go入门到精通/Go基础 + 进阶/
作者
where
发布于
2023年6月1日
许可协议