Skip to content

Commit 566ec31

Browse files
committed
modified go.md
1 parent 5857589 commit 566ec31

File tree

1 file changed

+49
-28
lines changed

1 file changed

+49
-28
lines changed

docs/backend/go/go.md

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Go
22

3-
Go 语言是一门由Google开发的**编译型**语言。其语法与 C 较为接近,OOP 水平介于 C 和 C++ 之间,特点是能比较轻易地进行并行编程。Go 被广泛地用于**后端开发**中,相比 Python, Node.js 等解释型语言往往拥有更高的运行效率。
3+
Go 语言是一门由Google开发的编译型语言。其语法与 C 较为接近,OOP 水平介于 C 和 C++ 之间,特点是能比较轻易地进行并行编程。Go 被广泛地用于**后端开发**中,相比 Python, Node.js 等解释型语言往往拥有更高的运行效率。
44

55
本文我们侧重介绍如何用 Go 搭建一个简易的 Web 服务器,并且完成与后端数据库通信的增删查改功能,对并发安全的内容亦会稍有介绍。
66

@@ -10,8 +10,8 @@ Credit: 基于 @pyz 的2022暑培讲稿,少量修改而成。
1010

1111
## 前置知识
1212

13-
+ 一种 C-family 语言
14-
+ 数据库的基本操作
13+
+ 一种 C-family 语言的使用经验
14+
+ MySQL 数据库的基础操作
1515
+ 对前后端分离架构的初步了解
1616

1717

@@ -20,7 +20,7 @@ Credit: 基于 @pyz 的2022暑培讲稿,少量修改而成。
2020

2121
### 1. Go 语言编译器安装
2222

23-
推荐访问 Go 语言官方网站的[下载页](https://golang.google.cn/doc/install)。在 Go install 的选项框里选择你的操作系统,下载打包好的安装文件并安装。不推荐install from source,较为繁琐。
23+
推荐访问 Go 语言官方网站的[下载页](https://golang.google.cn/doc/install)。在 Go install 的选项框里选择你的操作系统,下载打包好的安装文件并安装。不推荐 install from source,较为繁琐。
2424

2525
!!! note "安装提示"
2626

@@ -70,9 +70,11 @@ func main(){
7070

7171
- package声明部分
7272

73-
package 是 Go 语言管理代码的方式,我们一般把 package 成为“包”。package 和其它语言中的库或者模块(module)的地位类似。一般而言,一个包由一个或多个 .go 源文件组成。特别地,我们不以 *_test.go 的形式命名源文件,所有这样结尾的文件会被视为测试文件。
73+
package 是 Go 语言管理代码的方式,我们一般把 package 成为“包”。package 和其它语言中的库或者模块 (module) 的地位类似。一般而言,一个包由一个或多个 .go 源文件组成。特别地,我们不以 `*_test.go` 的形式命名源文件,所有这样结尾的文件会被视为测试文件。
7474

75-
每一个 package 的名字描述了这个 package 的功能,一般而言和这个 package 所处的文件目录的最后一级名字相同。main package是特殊的package,用来定义一个可执行的程序,这个可执行程序的执行起点是 main package 里面的 main 函数。
75+
每一个 package 的名字描述了这个 package 的功能,一般而言和这个 package 所处的文件目录的最后一级名字相同。
76+
77+
main package是特殊的package,用来定义一个可执行的程序,这个可执行程序的执行起点是 main package 里面的 `main` 函数。
7678

7779
- import部分
7880

@@ -171,7 +173,7 @@ func main() {
171173
var a int
172174
b := 0
173175
_, err := fmt.Scanf("%d %d",&a,&b)
174-
// _ 代表该函数有这一个返回值,但程序并不需要使用它,因此使用 _ 来忽略这一返回值。
176+
//使用_作为占位符,可以达到接受但不使用返回参数的目的
175177
if err != nil {
176178
//if 后的条件没有小括号,要和大括号在同一行内
177179
log.Fatal("Bad Input")
@@ -203,6 +205,7 @@ import(
203205
)
204206
205207
type (
208+
//Go语言的OOP是依靠方法第一个字母的大小写来判断的,大写开头公有,可以随意访问,小写开头私有
206209
StudentInfo struct {
207210
Name string `json:"name"`
208211
Score int `json:"score"`
@@ -216,19 +219,32 @@ type (
216219
217220
func main() {
218221
var data InfoList
222+
223+
//os.Open返回一个指向文件对象的指针和一个error,如选择使用返回的参数则必须接受所有的参数
219224
jsonFile, err := os.Open("info.json")
220225
if err != nil {
221226
log.Fatal(err)
222227
}
223-
bytedata, _ := ioutil.ReadAll(jsonFile)
228+
229+
bytedata, _ := ioutil.ReadAll(jsonFile) //使用_作为占位符,可以达到接受但不使用返回参数的目的
224230
err = json.Unmarshal(bytedata, &data)
225231
if err != nil {
226232
log.Fatal(err)
227233
}
228234
for _, info := range data.Infos {
235+
//range遍历可迭代对象,当只有一个变量时为索引遍历,两个变量时,第一个变量为索引,第二个为对象的拷贝
229236
fmt.Printf("student name: %s, score is %d\n", info.Name, info.Score)
230237
}
231238
}
239+
/*
240+
输出:
241+
student name: a, score is 100
242+
student name: b, score is 99
243+
student name: c, score is 60
244+
student name: d, score is 120
245+
student name: e, score is 100
246+
student name: f, score is 40
247+
*/
232248
```
233249

234250
以下为配套的`info.json`
@@ -441,7 +457,7 @@ c := func(a,b int) int {return a+b} (1,2)
441457

442458
### 2. 接收前端传来的参数
443459

444-
我们已经在前文 fancy版本的Web服务器 中展示了一种向后端传递请求参数的办法,这种json传参的方式一般搭配post方法使用。下文主要讲解另外2种传参的方式,其中 query string 和 json传参 是较为重要的,而 路径传参 作为知识补充。
460+
我们已经在前文 *fancy版本的Web服务器* 中展示了一种向后端传递请求参数的办法,这种json传参的方式一般搭配post方法使用。下文主要讲解另外2种传参的方式,其中 `query string``json传参` 是较为重要的,而`路径传参`作为知识补充。
445461

446462
```go
447463
package main
@@ -476,12 +492,6 @@ func HandleName(g *gin.Context) {
476492
}
477493
```
478494

479-
!!! notes "关于http状态码"
480-
481-
http状态码的作用是对http请求处理做一个概括,2~5开头的状态码分别对应:请求被正常接收和 理解(2xx)、请求需要客户端进一步执行操作(3xx)、请求有错误(4xx)、处理请求出现了服 务器侧的问题(5xx)。
482-
483-
具体到本届课涉及的代码,都应该返回200(statusOK),202这样的返回一般用于异步接口。
484-
485495

486496
运行如下脚本,可看到注释中的输出。
487497

@@ -507,9 +517,16 @@ echo
507517

508518
传参数的方法多种多样,在项目接口设计的时候,我们一般需要保持**设计的一致性**,尽量合乎大众的设计准则。
509519

520+
!!! notes "关于http状态码"
521+
522+
返回时,http状态码的作用是对http请求处理做一个概括,2~5开头的状态码分别对应:请求被正常接收和理解(2xx)、请求需要客户端进一步执行操作(3xx)、请求有错误(4xx)、处理请求出现了服务器侧的问题(5xx)。
523+
524+
具体到本届课涉及的代码,都应该返回200(statusOK),202这样的返回一般用于异步接口。
525+
526+
510527
### 3. 中间件与Cookie
511528

512-
当我们想完成一系列Handle function的时候,我们经常发现,我们需要重复很多的逻辑,比如,我们的网站要求登录的用户才有权访问,那么大量的页面都要有登录鉴权的逻辑,所以我们想把一些统一 的逻辑放在一起。更进一步的,我们希望通过鉴权机制完成更为高级的逻辑。
529+
当我们想完成一系列 Handle function 的时候,我们经常发现,我们需要重复很多的逻辑,比如,我们的网站要求登录的用户才有权访问,那么大量的页面都要有登录鉴权的逻辑,所以我们想把一些统一 的逻辑放在一起。更进一步的,我们希望通过鉴权机制完成更为高级的逻辑。
513530

514531
```go
515532
package main
@@ -598,19 +615,21 @@ func HandleLogin(g *gin.Context) {
598615

599616
!!! notes "关于Cookie"
600617

601-
使用访问者 ip 的 SHA256 作为 cookie ,这显然是不妥当的,但作为示例未尝不可。在实际场景中会有更稳妥的算法和合适的cookie过期机制。
618+
此处我们使用访问者 ip 的 SHA256 作为 cookie ,这显然是不妥当的,但作为示例未尝不可。在实际场景中会有更稳妥的算法和合适的cookie过期机制。
602619

603620
3. cookie鉴权和存储信息
604621

605622
`Verify()` 中间件使用 cookie 存在、且在 record 中作为合法登录的标志,实际应用中我们可能要考虑 cookie 是否过期等其他因素。两个数值操作 `accumulate``multiply` 则对用户的信息作了进一步更改与存储。
606623

607-
这看起来是一个“有记忆的”后端了,但这还远远不够——存储在内存里的信息随着掉点就将丢失。我们不希望b站服务器一停机,收藏夹里的东西没了,学校也不希望服务器一停电,学生成绩没了。
624+
这看起来是一个“有记忆的”后端了,但这还远远不够——存储在内存里的信息随着掉点就将丢失。我们不希望b站服务器一停机,收藏夹里的东西没了,学校也不希望服务器一停电,学生成绩没了。因此,我们需要用 gorm 来对接数据库。
608625

609626
当然cookie信息还有可能又被篡改的危险,我们需要更鲁棒的方式分级存储不同敏感程度的用户信息。我们需要我们的后端和数据库做交互。
610627

611628
### 4. gorm和gorm的automigrate
612629

613-
我们介绍 gorm 作为我们与数据库交互的框架, gorm 框架保留了 go 的并发性(如果出了bug,一般而言报错的 goroutine 不会是1号 goroutine ,即主进程),效率较高,同时较为人性化的维护了和数据库的连接,使编程时不必考虑保存问题。 gorm 采用了默认事务操作的机制,并发安全性较高,gorm 还提供了读写分离的支持,更适合大规模的业务。如果对数据库的“事务”概念不了解的话,可以把它理解为不受其他并行指令干扰的一系列指令。
630+
我们介绍 gorm 作为我们与数据库交互的框架, gorm 框架保留了 go 的并发性(如果出了bug,一般而言报错的 goroutine 不会是1号 goroutine ,即主进程),效率较高,同时较为人性化的维护了和数据库的连接,使编程时不必考虑保存问题。
631+
632+
gorm 采用了默认事务操作的机制,并发安全性较高,gorm 还提供了读写分离的支持,更适合大规模的业务。如果对数据库的“事务”概念不了解的话,可以把它理解为不受其他并行指令干扰的一系列指令。
614633

615634
下面提供一个简单的示例,展示gorm的使用。如果没有数据库相关的基础知识,可以先去阅读 MySQL 相关教程。
616635

@@ -647,11 +666,13 @@ func main() {
647666
}
648667
```
649668

650-
上述代码完成了与数据连接,迁移模型,新建一个条目三件事情。但是我们注意到,建立的 `student_infos` 数据表只有两列,`secert`一项丢失了,这是由于 GoOOP 特性,小写成员变量私有,这直接导致这一项将不会被gorm访问,存放到数据库中。
669+
上述代码完成了与数据连接,迁移模型,新建一个条目三件事情。
670+
671+
如果你亲自去运行这一段代码,并查看修改后的数据库,会注意到建立的 `student_infos` 数据表只有两列,`secert`一项丢失了,这是由于 GoOOP 特性,小写成员变量私有,这直接导致这一项将不会被 gorm 访问,存放到数据库中。
651672

652673
### 5. 增删查改和一些小技巧
653674

654-
这里仅仅展示最基本的增删查改,gorm提供了 `gorm.Model` 来支持软删除等高级模型操作,这里不做涉及。`Ops`函数中是增删查改操作。需要特别注意的一个方法是`update`不要采用选出数据库中条目,再使用`save`依靠主键冲突更新的方法完成
675+
这里仅仅展示最基本的增删查改,gorm 提供了 `gorm.Model` 来支持软删除等高级模型操作,这里不做涉及。`Ops`函数中是增删查改操作。需要特别注意的一个方法是`update`其可以用来修改已有条目的值——尽量不要采用选出数据库中条目,再使用`save`依靠主键冲突更新的方法完成这一操作
655676

656677
```go
657678
package main
@@ -773,7 +794,7 @@ func PrintTask1() {
773794
}
774795
```
775796

776-
运行程序,发现尽管所有Add函数都执行完成,但结果却远不及100000,而且每次的结果不一致。 这便是出现了**并发问题**!
797+
运行程序,发现尽管所有Add函数都执行完成,但结果却远不及预期的 10 * 10000 = 100000,而且每次的结果不一致。 这便是出现了**并发问题**!
777798

778799
如果使用 `go run -race main.go`go编译器会启动检测运行时竞争的编译模式。 -race 报警如下:
779800

@@ -807,9 +828,9 @@ concurrency/demo1/main.go:9 +0x29
807828

808829
简单来说,多个 goroutine 尝试修改内存中同一个变量的值。然而,修改某一变量的值并不是一个 “原子操作” ,对于最底层的处理器而言,需要多个步骤来完成(例如取值、对值进行运算、存储新值),因此会出现如下情况:
809830

810-
1. goroutine 1 想要将变量的值加 1因此取出了目前的变量值 a.
831+
1. goroutine 1 想要将变量的值加 1因此得到了目前的变量值 a.
811832

812-
2. goroutine 2 也想要将变量的值加 1,而此时 goroutine1 还没有完成对变量值的修改, 因此goroutine2也取出了目前的变量值 a.
833+
2. goroutine 2 也想要将变量的值加 1,而此时 goroutine1 还没有完成对变量值的修改, 因此goroutine2 也得到了目前的变量值 a.
813834

814835
3. 两个 goroutine 先后完成了运算、存储新值的过程,但是最终得到的变量值只是 a + 1, 与预期的 a + 2 不符。
815836

@@ -878,9 +899,9 @@ func PrintTask1() {
878899

879900
这是上述问题的另一个解决方案。
880901

881-
如果我们把为全局变量加一看作一个任务的话,我们可以专门设立一个完成这个任务的worker。其他进程通过channel和worker通信,把他们的任务交给这个worker来做
902+
如果我们把为全局变量加一看作一个任务的话,我们可以专门设立一个完成这个任务的 worker。其他进程通过 channel 和 worker 通信,把他们的任务交给这个 worker 来做
882903

883-
尽管在这个场景下,使用 worker 看起来有点多此一举,但是在后端搭建中,这样的思路是十分常见的。比如,我想完成一个从视频网站自动下载视频的存储器,我可能只希望在前端输入视频的名字,视频在后台完成下载,当前的页面应该有一个迅速的返回值。那么这个时候,就可以从我们的handle function中,通过管道,把视频相应的信息传给worker,由worker一个一个完成下载
904+
尽管在这个场景下,使用 worker 看起来有点多此一举,但是在后端搭建中,这样的思路是十分常见的。比如,我想完成一个从视频网站自动下载视频的存储器,我可能只希望在前端输入视频的名字,视频在后台完成下载,当前的页面应该有一个迅速的返回值。那么这个时候,就可以从我们的 handle function 中,通过管道,把视频相应的信息传给 worker ,由 worker 一个一个完成下载
884905

885906
```go
886907
package main
@@ -942,15 +963,15 @@ func Work() {
942963

943964
1. 直接编译可执行文件,在后端服务器上运行,这是naive的部署方法,主要适用于纯后端的场景。 当前端的负载均衡,前后端沟通在一个内网时,这就足够了。(软工课就是这样的)
944965

945-
2. 结合docker做部署,请参考网站内 docker 教程。
966+
2. 结合docker做部署,请参考网站内 docker 教程(可能尚未更新)
946967

947968

948969
## 后续拓展
949970

950971
+ 了解 restful-api 的概念 https://restfulapi.net/
951972
+ 了解 Go 后端的 redis
952973
+ 进行单元测试
953-
+ 深入了解 gin, gorm
974+
+ 深入了解 gin, gorm (可以参考资源链接中的材料)
954975

955976
## 资源链接
956977

0 commit comments

Comments
 (0)