Pro GO 笔记
Adam Freeman 的一贯风格, web 开发为模型.
第一部分 理解 Go 语言
ch01 第一个 Go 应用程序
做一个邀请的 web 应用, 记录前来人员的基本信息, 并打印欢迎页面.
安装开发工具
首先安装 go 开发工具, 验证安装是否成功使用 go version
来打印版本号. 作者使用的版本为 1.17.1
.
然后安装 Git 工具.
然后安装代码编辑工具. 作者推荐使用 VSCode
.
创建项目
使用命令
go mod init partyinvites
该命令会在第三章进行解释.
- 该命令会在当前目录下创建
go.mod
文件, 用于维护项目依赖的模块, 必要时发布项目也需要使用到它. - Go 代码文件使用
.go
后缀. 然后创建main.go
文件. 然后编写代码.
package main
import "fmt"
func main() {
fmt.Println("TODO: add some features")
}
- 代码结构也是 C 系的语法.
- 语言特性使用包来进行维护, 因此代码中引入了一个 包. 需要使用某包时, 使用
import
导入. 代码写在函数中, 使用关键字func
来定义函数. 代码中仅有一个函数, 名为main
. 该函数为程序入口. main
函数中仅有一句代码, 使用包fmt
中的Println
函数来进行输出.fmt
是 Go 标准库中提供的扩展, 会在第二部分详细讨论,Println
作用是输出一段字符串.- 要运行代码, 只需在
partyinvites
目录下执行命令go run .
即可.go run
命令会很常用, 它将编译与执行合成一个步骤.
如果运行得到一个错误, 表示代码并未按照要求来编写. 需要补充一点的是 Go
的开始的花括号应放在行尾, 而不是另起一行放在开头. 例如:
func main()
{
// ...
}
这会报错. 会提示没有函数体. 并会提示语法错误, 分号与空行不应该在 {
之前.
定义数据类型与集合
下一步是定义自定义数据类型.
type Rsvp struct {
Name, Email, Phone string
WillAttend bool
}
- 使用关键字
type
来定义类型. 上面创建一个名为Rsvp
的结构体. 结构体是将一些相关的数据集合在一起. 关键字string
,bool
是 Go 内置类型, 表示字符串与布尔.Rsvp
定义了四个字符串, 一个布尔值 (Go 的内置类型会在第4章介绍). - 然后需要存储
Rsvp
类型的集合. 后续章节会介绍使用数据库, 但这里暂时使用内存来存储数据. - Go 内置定长数组, 和边长数组 (例如,
slice
), 以及 map 来存储键值对. - 下面代码创建一个
slice
, 在未知存储数据个数的时候这样使用比较好.
var responses = make([]*Rsvp, 0, 10)
Go 内置了一些函数来处理数组 (array
), slice
, 以及 map
的一些公共行为. 其中一个函数是 make
, 这段代码中用于初始化 slice
. 后面两个参数用于初始化长度 (size), 以及容量 (capacity).
这里将 slice
长度初始化为 0
, 即表示为空集合. slice
是会根据存如的数据自动调整长度的, 其容量表示在超出该容量时才会自动调整长度.
第一个参数用于描述 slice
存储数据的类型. 其中方括号 []
表示它是一个 slice
, 星号 *
表示它是一个指针. 这里 Rsvp
表示使用前文定义的结构体, []*Rsvp
表示 slice
指项一系列 Rsvp
结构的实例.
不用担心指针的问题, Go 中不会在指针上进行一些运算操作, 它仅表示是否会有数据被复制. 这使用指针, 表示 Rsvp
实例是引用, 而非一个新副本.
最后将创建的 slice
赋值到变量 responses
上, 以便在其他地方可以使用.
关键字 var
用于声明变量, =
是赋值运算符. 这里并未指定变量类型, Go 编译器会自行从值中进行推断.
创建 HTML 模板
Go 自带了一个功能强大的标准库, 其中也包括对 html 模板的支持. 在 根目录下 添加文件 layout.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Let's Party!</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="p-2">
{{ block "body" . }} Content Goes Here {{ end }}
</body>
</html>
- 该文件定义了一个页面布局,算是一个公共的页面结构. 然后作者简要解释了这段代码的作用, 并说明 CDN, 作者会在第 24 章介绍如何从文件夹中获得资源, 但是在本章依旧会让代码离线使用, 所以这里 CSS 不会起作用.
- 双花括号
{{ ... }}
用于定义占位符, 动态内容会被填充到这里.block
表达式用于定义占位符, 在运行时会被替换掉.
添加 welcome.html
来创建欢迎用户的内容.
{{ define "body"}}
<div class="text-center">
<h3> We're going to have an exciting party!</h3>
<h4>And YOU are invited!</h4>
<a class="btn btn-primary" href="/form">
RSVP Now
</a>
</div>
{{ end }}
添加 form.html
来创建响应用户的内容
{{ define "body"}}
<div class="h5 bg-primary text-white text-center m-2 p-2">RSVP</div>
{{ if gt (len .Errors) 0}}
<ul class="text-danger mt-3">
{{ range .Errors }}
<li>{{ . }}</li>
{{ end }}
</ul>
{{ end }}
<form method="POST" class="m-2">
<div class="form-group my-1">
<label>Your name:</label>
<input name="name" class="form-control" value="{{.Name}}" />
</div>
<div class="form-group my-1">
<label>Your email:</label>
<input name="email" class="form-control" value="{{.Email}}" />
</div>
<div class="form-group my-1">
<label>Your phone number:</label>
<input name="phone" class="form-control" value="{{.Phone}}" />
</div>
<div class="form-group my-1">
<label>Will you attend?</label>
<select name="willattend" class="form-select">
<option value="true" {{if .WillAttend}}selected{{end}}>
Yes, I'll be there</option>
<option value="false" {{if not .WillAttend}}selected{{end}}>
No, I can't come</option>
</select>
</div>
<button class="btn btn-primary mt-3" type="submit">
Submit RSVP
</button>
</form>
{{ end }}
添加文件 thanks.html
来答谢提交表单的人
{{ define "body"}}
<div class="text-center">
<h1>Thank you, {{ . }}!</h1>
<div> It's great that you're coming. The drinks are already in the fridge!</div>
<div>Click <a href="/list">here</a> to see who else is coming.</div>
</div>
{{ end }}
添加 sorry.html
文件, 来表示拒绝提交表单的描述
{{ define "body"}}
<div class="text-center">
<h1>It won't be the same without you, {{ . }}!</h1>
<div>Sorry to hear that you can't make it, but thanks for letting us know.</div>
<div>
Click <a href="/list">here</a> to see who is coming,
just in case you change your mind.
</div>
</div>
{{ end }}
创建文件 list.html
来显示参会人员信息
{{ define "body"}}
<div class="text-center p-2">
<h2>Here is the list of people attending the party</h2>
<table class="table table-bordered table-striped table-sm">
<thead>
<tr><th>Name</th><th>Email</th><th>Phone</th></tr>
</thead>
<tbody>
{{ range . }}
{{ if .WillAttend }}
<tr>
<td>{{ .Name }}</td>
<td>{{ .Email }}</td>
<td>{{ .Phone }}</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
</div>
{{ end }}
加载模板
下一步是加载模板. 此时可能会出现代码编辑器的报错, 但不用担心, 后面会处理.
package main
import (
"fmt"
"html/template"
)
type Rsvp struct {
Name, Email, Phone string
WillAttend bool
}
var responses = make([]*Rsvp, 0, 10)
var templates = make(map[string]*template.Template, 3)
func loadTemplates() {
// TODO - 在这里加载模板
}
func main() {
loadTemplates()
}
- 第一个变化是
import
语句. 它声明了所需要的依赖html/template
包. 它是 Go 提供的标准库. 该包用于加载目标, 以及渲染 HTML 模板, 该模板会在 第23 章进行介绍. - 然后声明了变量
templates
.- 关键字
map
用于表示一个映射 (或键值对等). 其键的类型定义在方括号中, 是string
类型. 值的类型为*template.Template
, 表示值指向Template
结构类型, 该类型定义在template
包中. 在导入包后, 可以使用包名的最后一个部分来引用包中的成员. 例如本例中导入了html/template
, 那么可以使用template
来引用包中的成员Template
, 它是定义在包中的一个结构体. 这里的星号表示它是一个指针. 也就是说map
使用字符串的key
, 来存储指向Template
实例的指针. - 然后创建函数
loadTemplates
. 此时还没有编写代码, 后面会加载前面定义的模板, 并创建*Template
.Template
值会存储在map
中. 该函数会在main
内部执行. 你可以在代码文件中直接定义并初始化变量, 但是通常会在函数中进行完成.
- 关键字
下面来实现 loadTemplates
函数. 每一个模板都会伴随 layout
被加载. 也就是说不需要在每一个 html 文件中重复编写基本的 HTML.
有种
Razor
的感觉了.
func loadTemplates() {
templateNames := [5]string { "welcome", "form", "thanks", "sorry", "list" }
for index, name := range templateNames {
t, err := template.ParseFiles("layout.html", name + ".html")
if (err == nil) {
templates[name] = t
fmt.Println("Loaded template", index, name)
} else {
panic(err)
}
}
}
- Go 在函数中的赋值使用
:=
. 它声明变量templateNames
, 并使用:=
来初始化. [5]string { ... }
是数组的字面量, 在 Go 中数组是定长的, 不可变长度的. 这里使用前面创建的html
的文件名作为数组的元素.- 然后使用
for
循环来遍历数组. 其中使用了range
关键字.range
总是和for
用在一起, 用于枚举数组 (array
),slice
, 以及map
.for
循环内部的代码会为每一个元素执行一次 (就是循环), 本例中是数组, 并为每一个数组元素返回两个值:index
和name
.index
表示遍历元素的索引, 是int
类型, 它是 Go 的内置类型, 表示整数.name
表示索引为index
位置的元素, 它的类型与数组元素的类型一致, 这里数组是string
类型的, 因此name
也是string
类型. for
循环中的第一句话:template.ParseFiles(...)
是用来加载模板的.html/template
包中提供的ParseFiles
方法用来加载并处理模板. Go 中允许函数返回多个值, 这虽然不常见, 但是ParseFiles
就返回了两个, 一个是Template
的指针, 另一个是error
.error
也是 Go 内置的类型, 用于表示错误. 然后同时创建两个变量t
和err
来接收这两个值. 不用指定这两个变量的类型, 在编译阶段会自行推断. 这样对应初始化变量的方式是 Go 中的通用模式. 同时判断err
是否为nil
来判断是否出现错误, 来确保t
中是有值的.nil
是 Go 中空值 (null
) 的表示.- 如果
err == nil
, 将读取到的*template.Template
依据其名字存储到map
中. 如果不是nil
, 表示存在异常 (不可恢复的错误). Go 提供了一个函数panic
来输出错误信息 (逻辑上有点像try-catch
结构的功能).
使用 go run .
来运行程序.
创建 HTTP 处理程序与服务器
Go 标准库中内置了对 HTTP 服务器与处理 HTTP 请求的支持. 首先, 我们需要定义一些函数, 当用户请求默认 URL 的时候, 应用程序会调用它, 此时将默认 URL 定义为 /
. 当用户请求展示来宾时, 例如 /list
, 应用程序也会调用它.
import (
"fmt"
"html/template"
"net/http"
)
...
func welcomeHander(writer http.ResponseWriter, request *http.Request) {
templates["welcome"].Execute(writer, nil)
}
func listHandler(writer http.ResponseWriter, request *http.Request) {
templates["list"].Execute(writer, nil)
}
...
func main() {
...
http.HandleFunc("/", welcomHandler)
http.HandleFunc("/list", listHandler)
}
处理 HTTP 请求的功能定义在 net/http
包中, 它也是 Go 标准库中的一部分. 处理 http 请求的函数参数有固定格式. 例如 func welcome(writer http.ResponseWriter, request *http.Request)
.
- 第二个参数
request
是http.Response
指针, 指向Request
实例, 其定义在net/http
包中. 用于描述需要被处理的请求. 第一个参数是一个接口的示例 (它没有被定义成指针). 接口是方法结构类型的集合, 其细节会在第11章进行讨论. - 其中一个常用的接口是
Writer
. 它用于写入任意的数据, 例如文件, 字符串, 网络链接等.ResponseWriter
增加了额外的特性, 专门用于处理 HTTP 响应. - 定义在
*Template
中的Execute
方法来辅助写入. http
中定义的HandleFunc
方法来映射 URL 与处理程序.
到此演示已经足够, 其中细节, 例如如何处理请求, 将请求分发到不同的处理中会在后续进行介绍. 下面开始创建 HTTP 服务.
func main() {
...
err := http.ListenAndServe(":5000", nil)
if err!= nil {
panic(err)
}
}
http.ListenAndServe
用于启用监听的功能,第一个参数用于绑定监听的 IP 与端口, 第二个参数通常是 nil
, 实际上可以是一个处理函数, 表示在接收请求时需要第一处理的逻辑 (逻辑上类似于一个切片, 或管道的开始). 如果程序监听失败会返回一个 error
, 如果成功会阻塞等待请求.
使用 go run .
来运行项目. 然后可以在浏览器中使用 http://localhost:5000/
或 http://localhost:5000/list
来访问查看.
使用 ctrl + c
来结束程序.
编写表单处理函数
点击 RSVP Now
后应跳转到 /form
来收集用户的信息.
- 需要定义新的函数来处理
/form
请求 - 表单需要定义新的模型 (
struct
) 来绑定需要的数据 - 在处理函数中, 首先判断请求的类型 (
GET
或POST
), 来确定是显示表单信息, 还是处理表单数据
type formData struct {
*Rsvp
Errors []string
}
func formHandler(writer http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodGet {
templates["form"].Execute(writer, formData {
Rsvp: &Rsvp{},
Errors: []string{},
})
}
}
func main() {
...
http.HandleFunc("/form", formHandler)
}
form.html
需要接收指定类型的数据来对页面表单内容进行渲染. 为了实现该功能, 我们定义了类型formData
. Go 的结构体除了支持键值对的形式来声明其其成员, 还可以直接利用已有的数据类型来定义成员, 这里使用指针指向已存在的Rsvp
结构.因此
formData
结构可以使用Rsvp
中定义的Name
,Email
, ... 等成员. 同时可以使用已存在的Rsvp
值来创建formData
实例. 星号表示它是一个指针, 也就是说在创建formData
实例的时候不会创建新的数据副本.新的处理函数检查请求方法为 GET 请求, 来渲染
form
页面. 处理 GET 请求时不需要数据渲染页面, 但是依旧需要一个空数据来进行渲染, 这里使用默认值来创建了formData
实例. Go 中没有new
关键字, 并且使用花括号{}
来创建值. 在没有为成员指定特殊值时, 均使用默认值.Rsvp{}
创建了Rsvp
的实例, 空的花括号表示所有成员使用默认值.&
表示创建一个指向该值的指针.
逻辑上与模板引擎很像.
例外似乎
类型 { ... }
就是在实例化对象, 而成员的初始化与JSON
类似.
然后使用 go run .
来运行项目.
处理表单数据
然后需要处理 POST 请求, 并在该请求处理中获得用户在表单中输入的数据. 代码片段为:
func formHandler(writer http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodGet {
...
} else if request.Method == http.MethodPost {
request.ParseForm()
responseData := Rsvp {
Name: request.Form["name"][0],
Email: request.Form["email"][0],
Phone: request.Form["phone"][0],
WillAttend: request.Form["willattend"][0] == "true",
}
responses = append(responses, &responseData)
if responseData.WillAttend {
templates["thanks"].Execute(writer, responseData.Name)
} else {
templates["sorry"].Execute(writer, responseData.Name)
}
}
}
ParseForm
方法处理包含在 HTTP 请求中的表单数据, 并将其填充到一个 map
中, 可以通过 request.Form
来引用该 map
. 最后使用这些数据来初始化 Rsvp
对象.
request.ParseForm()
responseData := Rsvp {
Name: request.Form["name"][0],
Email: request.Form["email"][0],
Phone: request.Form["phone"][0],
WillAttend: request.Form["willattend"][0] == "true",
}
上述代码描述了如和使用数据来实例化一个对象.
- HTML 表单允许一个名字有多个值, 因此在获取表单数据时, Go 使用
slice
数据类型, 即使我们知道每一个名字只有一个值. Go 中使用 base-0 的索引机制, 来获得对应的表单值. - 在实例化
Rsvp
对象后, 将其追加到slice
中, 使用append
方法. 该方法将数据追加到slice
中. 需要注意的是这里使用了取地址符号 (&
), 如果没有该运算, 则会导致数据的重复. - 最后根据
WillAttend
来判断最终使用什么模板来呈现结果.
最后运行代码 go run .
添加数据校验
添加数据校验是在 POST 请求来到后, 对用户输入的数据进行校验
func formHandler(writer http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodGet {
templates["form"].Execute(writer, formData {
Rsvp: &Rsvp{},
Errors: []string{},
})
} else if request.Method == http.MethodPost {
request.ParseForm()
responseData := Rsvp {
Name: request.Form["name"][0],
Email: request.Form["email"][0],
Phone: request.Form["phone"][0],
WillAttend: request.Form["willattend"][0] == "true",
}
errors := []string{}
if responseData.Name == "" {
errors = append(errors, "Please enter your name")
}
if responseData.Email == "" {
errors = append(errors, "Please enter your email")
}
if responseData.Phone == "" {
errors = append(errors, "Please enter your phone number")
}
if len(errors) > 0 {
templates["form"].Execute(writer, formData {
Rsvp: &responseData,
Errors: errors,
})
} else {
responses = append(responses, &responseData)
if responseData.WillAttend {
templates["thanks"].Execute(writer, responseData.Name)
} else {
templates["sorry"].Execute(writer, responseData.Name)
}
}
}
}
运行 go run .
小结
简单总结了一下 Go 的安装与 web 用法. 下一章将 go 置于上下文中进行讨论 (感觉应该翻译成将 go 放在实际应用中来学习).
ch02 实际应用中的 Go
- Go 常常被引用为 Golang, 是由 Google 开发的语言.
- 它的语法类似于 C, 但 Go 有安全的指针, 自动的内存管理, 以及常用的标准库.
为什么要学习 Go
- Go 更加适用于服务开发或系统开发. 其标准库提供了很多服务端的支持. 它对线程的支持很强大, 并带有丰富的反射功能.
- 它带有完善的开发工具集.
- 它是跨平台的, 并且基于 Docker, 可以更加灵活.
What's the Catch?
略
Is It Really That Bad?
略
你需要知道的
本书需要一些开发经验.
本书的结构
本书分为三部分.
第一部分 理解 Go 语言
主要介绍开发工具, 与 Go 语言本身. 其中包括内置数据类型, 如何创建自定义类型, 还有流程控制, 异常处理, 以及并发. 本部分为了描述与演示也会包含一部分标准库的内容.
第二部分 使用 Go 标准库
主要介绍 Go 标准库中的常用包. 包括字符串格式化, 读写数据, 创建 HTTP 服务端与客户端. 使用数据库, 以及反射.
第三部分 应用 Go
该部分使用 Go 创建了一个自定义 web 应用程序. 即 SportStore.
本书不包含什么
本书不会包含所有 Go 的语法特征以及所有的标准库. 仅仅包含最常用的. 如果有些你需要, 但本书不包含的, 你可以联系作者.
如果发现错误怎么办?
略
示例
主要介绍排版规则. 略
需要的软件
只需要第一章介绍的 Go. 后续会安装一些第三方扩展, 但依旧会使用 go 命令来安装. 第三部分会使用 Docker.
需要的平台
Windows 和 Linux (ubuntu 20.04).
对于示例你有问题怎么处理?
略
在哪里获得示例代码
略
为什么代码会有奇怪格式
为了便于数据排版, 会折行处理. 略.
我该如何联系作者
略
其他...