1
1
# Go
2
2
3
- Go 语言是一门由Google开发的 ** 编译型 ** 语言 。其语法与 C 较为接近,OOP 水平介于 C 和 C++ 之间,特点是能比较轻易地进行并行编程。Go 被广泛地用于** 后端开发** 中,相比 Python, Node.js 等解释型语言往往拥有更高的运行效率。
3
+ Go 语言是一门由Google开发的编译型语言 。其语法与 C 较为接近,OOP 水平介于 C 和 C++ 之间,特点是能比较轻易地进行并行编程。Go 被广泛地用于** 后端开发** 中,相比 Python, Node.js 等解释型语言往往拥有更高的运行效率。
4
4
5
5
本文我们侧重介绍如何用 Go 搭建一个简易的 Web 服务器,并且完成与后端数据库通信的增删查改功能,对并发安全的内容亦会稍有介绍。
6
6
@@ -10,8 +10,8 @@ Credit: 基于 @pyz 的2022暑培讲稿,少量修改而成。
10
10
11
11
## 前置知识
12
12
13
- + 一种 C-family 语言
14
- + 数据库的基本操作
13
+ + 一种 C-family 语言的使用经验
14
+ + MySQL 数据库的基础操作
15
15
+ 对前后端分离架构的初步了解
16
16
17
17
@@ -20,7 +20,7 @@ Credit: 基于 @pyz 的2022暑培讲稿,少量修改而成。
20
20
21
21
### 1. Go 语言编译器安装
22
22
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,较为繁琐。
24
24
25
25
!!! note "安装提示"
26
26
@@ -70,9 +70,11 @@ func main(){
70
70
71
71
- package声明部分
72
72
73
- package 是 Go 语言管理代码的方式,我们一般把 package 成为“包”。package 和其它语言中的库或者模块(module)的地位类似。一般而言,一个包由一个或多个 .go 源文件组成。特别地,我们不以 * _ test.go 的形式命名源文件,所有这样结尾的文件会被视为测试文件。
73
+ package 是 Go 语言管理代码的方式,我们一般把 package 成为“包”。package 和其它语言中的库或者模块 (module) 的地位类似。一般而言,一个包由一个或多个 .go 源文件组成。特别地,我们不以 ` *_test.go ` 的形式命名源文件,所有这样结尾的文件会被视为测试文件。
74
74
75
- 每一个 package 的名字描述了这个 package 的功能,一般而言和这个 package 所处的文件目录的最后一级名字相同。main package是特殊的package,用来定义一个可执行的程序,这个可执行程序的执行起点是 main package 里面的 main 函数。
75
+ 每一个 package 的名字描述了这个 package 的功能,一般而言和这个 package 所处的文件目录的最后一级名字相同。
76
+
77
+ main package是特殊的package,用来定义一个可执行的程序,这个可执行程序的执行起点是 main package 里面的 ` main ` 函数。
76
78
77
79
- import部分
78
80
@@ -171,7 +173,7 @@ func main() {
171
173
var a int
172
174
b := 0
173
175
_, err := fmt.Scanf("%d %d",&a,&b)
174
- // _ 代表该函数有这一个返回值,但程序并不需要使用它,因此使用 _ 来忽略这一返回值。
176
+ //使用_作为占位符,可以达到接受但不使用返回参数的目的
175
177
if err != nil {
176
178
//if 后的条件没有小括号,要和大括号在同一行内
177
179
log.Fatal("Bad Input")
@@ -203,6 +205,7 @@ import(
203
205
)
204
206
205
207
type (
208
+ //Go语言的OOP是依靠方法第一个字母的大小写来判断的,大写开头公有,可以随意访问,小写开头私有
206
209
StudentInfo struct {
207
210
Name string ` json:" name" `
208
211
Score int ` json:" score" `
@@ -216,19 +219,32 @@ type (
216
219
217
220
func main() {
218
221
var data InfoList
222
+
223
+ //os.Open返回一个指向文件对象的指针和一个error,如选择使用返回的参数则必须接受所有的参数
219
224
jsonFile, err := os.Open("info.json")
220
225
if err != nil {
221
226
log.Fatal(err)
222
227
}
223
- bytedata, _ := ioutil.ReadAll(jsonFile)
228
+
229
+ bytedata, _ := ioutil.ReadAll(jsonFile) //使用_作为占位符,可以达到接受但不使用返回参数的目的
224
230
err = json.Unmarshal(bytedata, &data)
225
231
if err != nil {
226
232
log.Fatal(err)
227
233
}
228
234
for _, info := range data.Infos {
235
+ //range遍历可迭代对象,当只有一个变量时为索引遍历,两个变量时,第一个变量为索引,第二个为对象的拷贝
229
236
fmt.Printf("student name: %s, score is %d\n", info.Name, info.Score)
230
237
}
231
238
}
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
+ */
232
248
` ` `
233
249
234
250
以下为配套的` info.json`
@@ -441,7 +457,7 @@ c := func(a,b int) int {return a+b} (1,2)
441
457
442
458
### 2 . 接收前端传来的参数
443
459
444
- 我们已经在前文 fancy版本的Web服务器 中展示了一种向后端传递请求参数的办法,这种json传参的方式一般搭配post方法使用。下文主要讲解另外2 种传参的方式,其中 query string 和 json传参 是较为重要的,而 路径传参 作为知识补充。
460
+ 我们已经在前文 * fancy版本的Web服务器* 中展示了一种向后端传递请求参数的办法,这种json传参的方式一般搭配post方法使用。下文主要讲解另外2 种传参的方式,其中 ` query string` 和 ` json传参` 是较为重要的,而` 路径传参` 作为知识补充。
445
461
446
462
` ` ` go
447
463
package main
@@ -476,12 +492,6 @@ func HandleName(g *gin.Context) {
476
492
}
477
493
` ` `
478
494
479
- !!! notes " 关于http状态码"
480
-
481
- http状态码的作用是对http请求处理做一个概括,2 ~5 开头的状态码分别对应:请求被正常接收和 理解(2xx)、请求需要客户端进一步执行操作(3xx)、请求有错误(4xx)、处理请求出现了服 务器侧的问题(5xx)。
482
-
483
- 具体到本届课涉及的代码,都应该返回200 (statusOK),202 这样的返回一般用于异步接口。
484
-
485
495
486
496
运行如下脚本,可看到注释中的输出。
487
497
@@ -507,9 +517,16 @@ echo
507
517
508
518
传参数的方法多种多样,在项目接口设计的时候,我们一般需要保持**设计的一致性**,尽量合乎大众的设计准则。
509
519
520
+ !!! notes " 关于http状态码"
521
+
522
+ 返回时,http状态码的作用是对http请求处理做一个概括,2 ~5 开头的状态码分别对应:请求被正常接收和理解(2xx)、请求需要客户端进一步执行操作(3xx)、请求有错误(4xx)、处理请求出现了服务器侧的问题(5xx)。
523
+
524
+ 具体到本届课涉及的代码,都应该返回200 (statusOK),202 这样的返回一般用于异步接口。
525
+
526
+
510
527
### 3 . 中间件与Cookie
511
528
512
- 当我们想完成一系列Handle function的时候 ,我们经常发现,我们需要重复很多的逻辑,比如,我们的网站要求登录的用户才有权访问,那么大量的页面都要有登录鉴权的逻辑,所以我们想把一些统一 的逻辑放在一起。更进一步的,我们希望通过鉴权机制完成更为高级的逻辑。
529
+ 当我们想完成一系列 Handle function 的时候 ,我们经常发现,我们需要重复很多的逻辑,比如,我们的网站要求登录的用户才有权访问,那么大量的页面都要有登录鉴权的逻辑,所以我们想把一些统一 的逻辑放在一起。更进一步的,我们希望通过鉴权机制完成更为高级的逻辑。
513
530
514
531
` ` ` go
515
532
package main
@@ -598,19 +615,21 @@ func HandleLogin(g *gin.Context) {
598
615
599
616
!!! notes " 关于Cookie"
600
617
601
- 使用访问者 ip 的 SHA256 作为 cookie ,这显然是不妥当的,但作为示例未尝不可。在实际场景中会有更稳妥的算法和合适的cookie过期机制。
618
+ 此处我们使用访问者 ip 的 SHA256 作为 cookie ,这显然是不妥当的,但作为示例未尝不可。在实际场景中会有更稳妥的算法和合适的cookie过期机制。
602
619
603
620
3 . cookie鉴权和存储信息
604
621
605
622
` Verify()` 中间件使用 cookie 存在、且在 record 中作为合法登录的标志,实际应用中我们可能要考虑 cookie 是否过期等其他因素。两个数值操作 ` accumulate` 和 ` multiply` 则对用户的信息作了进一步更改与存储。
606
623
607
- 这看起来是一个“有记忆的”后端了,但这还远远不够——存储在内存里的信息随着掉点就将丢失。我们不希望b站服务器一停机,收藏夹里的东西没了,学校也不希望服务器一停电,学生成绩没了。
624
+ 这看起来是一个“有记忆的”后端了,但这还远远不够——存储在内存里的信息随着掉点就将丢失。我们不希望b站服务器一停机,收藏夹里的东西没了,学校也不希望服务器一停电,学生成绩没了。因此,我们需要用 gorm 来对接数据库。
608
625
609
626
当然cookie信息还有可能又被篡改的危险,我们需要更鲁棒的方式分级存储不同敏感程度的用户信息。我们需要我们的后端和数据库做交互。
610
627
611
628
### 4 . gorm和gorm的automigrate
612
629
613
- 我们介绍 gorm 作为我们与数据库交互的框架, gorm 框架保留了 go 的并发性(如果出了bug,一般而言报错的 goroutine 不会是1 号 goroutine ,即主进程),效率较高,同时较为人性化的维护了和数据库的连接,使编程时不必考虑保存问题。 gorm 采用了默认事务操作的机制,并发安全性较高,gorm 还提供了读写分离的支持,更适合大规模的业务。如果对数据库的“事务”概念不了解的话,可以把它理解为不受其他并行指令干扰的一系列指令。
630
+ 我们介绍 gorm 作为我们与数据库交互的框架, gorm 框架保留了 go 的并发性(如果出了bug,一般而言报错的 goroutine 不会是1 号 goroutine ,即主进程),效率较高,同时较为人性化的维护了和数据库的连接,使编程时不必考虑保存问题。
631
+
632
+ gorm 采用了默认事务操作的机制,并发安全性较高,gorm 还提供了读写分离的支持,更适合大规模的业务。如果对数据库的“事务”概念不了解的话,可以把它理解为不受其他并行指令干扰的一系列指令。
614
633
615
634
下面提供一个简单的示例,展示gorm的使用。如果没有数据库相关的基础知识,可以先去阅读 MySQL 相关教程。
616
635
@@ -647,11 +666,13 @@ func main() {
647
666
}
648
667
` ` `
649
668
650
- 上述代码完成了与数据连接,迁移模型,新建一个条目三件事情。但是我们注意到,建立的 ` student_infos` 数据表只有两列,` secert` 一项丢失了,这是由于 Go 的 OOP 特性,小写成员变量私有,这直接导致这一项将不会被gorm访问,存放到数据库中。
669
+ 上述代码完成了与数据连接,迁移模型,新建一个条目三件事情。
670
+
671
+ 如果你亲自去运行这一段代码,并查看修改后的数据库,会注意到建立的 ` student_infos` 数据表只有两列,` secert` 一项丢失了,这是由于 Go 的 OOP 特性,小写成员变量私有,这直接导致这一项将不会被 gorm 访问,存放到数据库中。
651
672
652
673
### 5 . 增删查改和一些小技巧
653
674
654
- 这里仅仅展示最基本的增删查改,gorm提供了 ` gorm.Model` 来支持软删除等高级模型操作,这里不做涉及。` Ops` 函数中是增删查改操作。需要特别注意的一个方法是` update` ,不要采用选出数据库中条目 ,再使用` save` ,依靠主键冲突更新的方法完成 。
675
+ 这里仅仅展示最基本的增删查改,gorm 提供了 ` gorm.Model` 来支持软删除等高级模型操作,这里不做涉及。` Ops` 函数中是增删查改操作。需要特别注意的一个方法是` update` ,其可以用来修改已有条目的值——尽量不要采用选出数据库中条目 ,再使用` save` ,依靠主键冲突更新的方法完成这一操作 。
655
676
656
677
` ` ` go
657
678
package main
@@ -773,7 +794,7 @@ func PrintTask1() {
773
794
}
774
795
` ` `
775
796
776
- 运行程序,发现尽管所有Add函数都执行完成,但结果却远不及 100000 ,而且每次的结果不一致。 这便是出现了**并发问题**!
797
+ 运行程序,发现尽管所有Add函数都执行完成,但结果却远不及预期的 10 * 10000 = 100000 ,而且每次的结果不一致。 这便是出现了**并发问题**!
777
798
778
799
如果使用 ` go run -race main.go` ,go 编译器会启动检测运行时竞争的编译模式。 -race 报警如下:
779
800
@@ -807,9 +828,9 @@ concurrency/demo1/main.go:9 +0x29
807
828
808
829
简单来说,多个 goroutine 尝试修改内存中同一个变量的值。然而,修改某一变量的值并不是一个 “原子操作” ,对于最底层的处理器而言,需要多个步骤来完成(例如取值、对值进行运算、存储新值),因此会出现如下情况:
809
830
810
- 1 . goroutine 1 想要将变量的值加 1 , 因此取出了目前的变量值 a.
831
+ 1 . goroutine 1 想要将变量的值加 1 , 因此得到了目前的变量值 a.
811
832
812
- 2 . goroutine 2 也想要将变量的值加 1 ,而此时 goroutine1 还没有完成对变量值的修改, 因此goroutine2也取出了目前的变量值 a.
833
+ 2 . goroutine 2 也想要将变量的值加 1 ,而此时 goroutine1 还没有完成对变量值的修改, 因此goroutine2 也得到了目前的变量值 a.
813
834
814
835
3 . 两个 goroutine 先后完成了运算、存储新值的过程,但是最终得到的变量值只是 a + 1 , 与预期的 a + 2 不符。
815
836
@@ -878,9 +899,9 @@ func PrintTask1() {
878
899
879
900
这是上述问题的另一个解决方案。
880
901
881
- 如果我们把为全局变量加一看作一个任务的话,我们可以专门设立一个完成这个任务的worker。其他进程通过channel和worker通信,把他们的任务交给这个worker来做 。
902
+ 如果我们把为全局变量加一看作一个任务的话,我们可以专门设立一个完成这个任务的 worker。其他进程通过 channel 和 worker 通信,把他们的任务交给这个 worker 来做 。
882
903
883
- 尽管在这个场景下,使用 worker 看起来有点多此一举,但是在后端搭建中,这样的思路是十分常见的。比如,我想完成一个从视频网站自动下载视频的存储器,我可能只希望在前端输入视频的名字,视频在后台完成下载,当前的页面应该有一个迅速的返回值。那么这个时候,就可以从我们的handle function中 ,通过管道,把视频相应的信息传给worker,由worker一个一个完成下载 。
904
+ 尽管在这个场景下,使用 worker 看起来有点多此一举,但是在后端搭建中,这样的思路是十分常见的。比如,我想完成一个从视频网站自动下载视频的存储器,我可能只希望在前端输入视频的名字,视频在后台完成下载,当前的页面应该有一个迅速的返回值。那么这个时候,就可以从我们的 handle function 中 ,通过管道,把视频相应的信息传给 worker ,由 worker 一个一个完成下载 。
884
905
885
906
` ` ` go
886
907
package main
@@ -942,15 +963,15 @@ func Work() {
942
963
943
964
1 . 直接编译可执行文件,在后端服务器上运行,这是naive的部署方法,主要适用于纯后端的场景。 当前端的负载均衡,前后端沟通在一个内网时,这就足够了。(软工课就是这样的)
944
965
945
- 2 . 结合docker做部署,请参考网站内 docker 教程。
966
+ 2 . 结合docker做部署,请参考网站内 docker 教程(可能尚未更新) 。
946
967
947
968
948
969
## 后续拓展
949
970
950
971
+ 了解 restful-api 的概念 https:// restfulapi.net/
951
972
+ 了解 Go 后端的 redis
952
973
+ 进行单元测试
953
- + 深入了解 gin, gorm
974
+ + 深入了解 gin, gorm (可以参考资源链接中的材料)
954
975
955
976
## 资源链接
956
977
0 commit comments