这本书主要讲了使用golang原生的
net/http
包开发Web应用,后面也强调了使用goroutine
和channel
实现并发处理,提高Web应用的性能。是一本比较系统的介绍Go Web开发的书。本文按照全书行进章节进行一些要点的记录。
1. Go与Web应用
- 大规模可扩展的Web应用应具备以下特质:
- 可扩展:垂直扩展(提升单台设备的CPU数量和性能)、水平扩展(增加计算机的数量提高性能),go的并发编程的特点让其在垂直扩展上有优势。
- 模块化:接口实现动态类型匹配、函数闭包等,有助于模块化设计;go常用于构建微服务。
- 可维护:语法简洁可读,工具链如
gotest
较完备。 - 高性能:编译型静态代码,速度快于解释型语言,并发编程也有利于高性能。
- Web工作的原理:主要理解服务器与客户端的通信原理,包括HTTP、TCP/IP等协议。这里要注意,HTTP是无状态的,所以引出后面
cookie
和session
的使用,来保存用户状态。 - HTTP请求:要熟悉请求报文,如请求头、首部、报文主体。
- 请求方法:GET、POST、DELETE、HEAD、PUT等,在关于Restful API构建是,这些方法是要对应不同的服务。
- 安全请求方法:如Get、HEAD不会对服务器的状态进行修改,是安全的;而POST、PUT、DELETE会对服务器状态修改,这些方法都是不安全的。
- 幂等请求方法:就是一个HTTP方法使用相同数据进行二次调用时,不会对服务器的状态造成任何改变,那这个方法就是幂等的。所有安全请求方法都是幂等的,PUT和DELETE也是幂等的。
- HTTP请求首部:记录了与请求本身及客户端有关的信息,许多字段如Accept、Cookie、Content-Length、Content-Type、User-Agent等,这些都是需要熟悉的。
- HTTP响应:是对HTTP请求报文的响应,报文包括状态行、零个和任意响应首部、可选报文主体。
- 响应状态码:在状态行中,有1XX、2XX、3XX、4XX、5XX五类,有不同的含义。开发中,对请求的正常或者异常的响应都有状态码是一个很好的习惯。
- 响应首部:如Content-Length、Content-Type、Set-Cookie等。
- HTTP/2:HTTP/1是纯文本方式、HTTP/2是二进制协议,语法分析更为高效,协议更加紧凑和健壮。HTTP 1.X一次只能发单个请求,而HTTP 2是完全多路复用的,多个请求和响应可以在同一时间内使用同一个连接,有效的提高性能。
- Web应用通过HTTP协议,以HTTP请求报文的形式获取客户端输入->对请求报文进行处理后执行必要的操作->生成HTML以HTTP响应报文的形式返回给客户端。为了完成这些任务,Web应用可以被分为处理器Handler和模板引擎Template Engine两部分。
- 处理器:处理器接收请求,处理,调用模板引擎填充数据。在MVC模式看来处理器既是controller又是model。
- 模板引擎:接收处理器的数据,渲染成HTML返回给客户端。有静态模板和动态模板两种,静态模板用占位符替换成相应的数据生成HTML,动态模板除了使用占位符,还有编程语句结构,如条件语句、迭代、变量之类的。
Go的net/http
net/http
标准库可以分为客户端和服务器两部分,有的结构和函数只支持客户端和服务器中的一个,有的则同时支持客户端和服务器。
一般的有两种方式配置Web服务器:
// 方式一
http.ListenAndServe("", nil)
// 方式二 更详细的配置服务器
server := http.Server{
Addr: "127.0.0.1:8080",
Handler: nil, // 使用的处理器
ReadTimeout: ...
...
}
server.ListenAndServe()
关于HTTPS
使用HTTPS需要证书和密钥:
server.ListenAndServeTLS("cert.pem", "key.pem")
其中,cert.pem
是SSL证书,key.pem
是服务器的私钥。实际的生产环境中使用SSL证书需要通过VeriSign、Thawte等一些CA取得。通过Go的crypto
包可以生成证书。
2. 一个通用的Web应用
一个通用的Web应用从设计开始,包括下面一些方面:
- 使用多路复用器配置处理器、处理器函数
- 数据模型,包括数据结构、存储等
- 静态文件如HTML、CSS
- 使用cookie和session进行访问控制
- 使用模板生成HTML响应
使用Cookie进行访问控制
用户登录成功身份经过验证后,服务器会在成功的登录请求的响应的首部中写入一个cookie,客户端在接收这个cookie后将它写入存储到浏览器中。Go中的http.Cookie
用来构建cookie。核实用户身份后,程序在后端创建一个session,存储到数据库中,session结构中的uuid
字段作为cookie的值,返回给客户端存储在浏览器里,之后生命周期里浏览器的请求会带着cookie,完成访问控制。
对于cookie的生命周期一般有两类:
会话cookie:不设置特定的过期时间,在浏览器关闭后自动移除失效。
通过设置过期时间的cookie:比如5min后过期。
cookie := http.Cookie{
Name: "_cookie",
Value: session.Uuid,
HttpOnly: true,
}
http.SetCookie(writer, &cookie) // 写到响应首部
HttpOnly
字段表示该cookie只能通过HTTP/HTTPS访问,无法通过JavaScript等非HTTP API访问。
在后续客户端发来请求后,服务器从Request中拿到cookie的value,也就是存储在数据库中session对象的Uuid,进行比对,进而识别请求的客户端身份是否合法。
// 拿到cookie进行访问控制处理
cookie := r.Cookie("_cookie")
sess = data.Session{Uuid: cookie.Value}
ok := sess.Check()
if ok {
...
}
3. 使用处理器和处理器函数接收请求
处理器和处理器函数
处理器
Go中,一个处理器就是一个拥有ServeHTTP(http.ResponWriter, *http.Request)
方法的接口。而多路复用器DefaultServerMux
是ServeMux
的一个实例,后者也实现了ServeHTTP
方法。DefaultServerMux
既是ServeMux
的实例,也是Handler
结构的实例,它不仅是一个多路复用器还是一个处理器。它要做的事情就是根据请求的URL重定向到不同的处理器上。
当然,我们也可以编写自己的处理器去代替默认的DefaultServerMux
。更多的时候,我们会使用多个处理器去处理指定的URL,所以常常使用服务器默认的DefaultServerMux
做多路复用器,将不同的处理器使用http.Handle
绑定到DefaultServerMux
。http.Handle
实际上是ServeMux
结构的方法:
// 定义一个处理器
type HelloHandler struct{}
// 实现 ServeHTTP 接口
func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
...
}
// http.Handle 也做多路复用器的用途
http.Handle("/hello", &hello) // 传入一个处理器处理来自 /hello 的路由
处理器函数
处理器函数是与处理器拥有相同行为的函数,与ServeHTTP有相同的签名。
func hello(w http.ResponseWriter, r *http.Request) {
...
}
http.HandleFunc("/hello", hello)
实际上,Go的HandlerFunc
函数类型,把一个处理器函数隐式的转换成一个处理器。
// 源码
// 将处理器函数给默认的多路复用处理器
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
// 调用 多路复用处理器的 HandleFunc 对路由绑定处理器
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
// mux.Handle(pattern, HandlerFunc(handler)) 里面的 HandlerFunc() 实现了 ServeHTTP
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
串联多个处理器和处理器函数
常见的Web在真正处理数据时,往往需要做一些其他工作,如:日志记录、安全检查、错误处理等。有些时候需要在处理业务的处理器前面串联上其他的处理器,又叫做管道处理。得益于Go的匿名函数、闭包这些特性,可以很好的将处理器串联起来。
串联多个处理器函数
// 串联多个处理器函数
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello")
}
// 在处理hello前串联一个记录日志的处理器
func log(h http.HandlerFunc) http.HandlerFunc {
return func (w http.ResponseWriter, r *http.Request) {
// 处理日志
...
h(w, r)
}
}
// 在处理日志之前再串联一个验证用户身份的处理器
func authentication(h http.HandlerFunc) http.HandlerFunc {
return func (w http.ResponseWriter, r *http.Request) {
// 鉴权
...
h(w, r)
}
}
// main
func main() {
...
http.HandleFunc("/hello", authentication(log(hello)))
...
}
串联处理器
type HelloHandler struct{}
func (h HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello!")
}
func log(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Handler called - %T\n", h)
// 处理器直接显示调用ServeHTTP
h.ServeHTTP(w, r)
})
}
func authentication(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Do some protect...")
h.ServeHTTP(w, r)
})
}
func main() {
...
http.Handle("/hello", authentication(log(hello)))
...
}
与串联处理器函数不同的是,串联处理器需要将匿名函数返回的函数显式的使用http.HandlerFunc
转换成http.Handler
。在main
中使用的也是http.Handle()
而不是http.HandleFunc()
了。
ServeMux和DefaultServeMux
ServeMux将URL映射到响应的处理器。它也实现了ServeHTTP
方法。
而DefaultServeMux是ServeMux的一个实例。开发中没有指定处理器是,Go的服务器就会用它作为默认实例。
最小惊讶原则
从上图,假设有:
http.Handle("/hello", &helloHandler)
请求的URL为/hello
,多路复用器会分发到helloHandler
处理,若为/hello/other
呢?
答案也是一样:helloHandler
。就类似于最长前缀匹配规则。
但是,如果改成:
http.Handle("/hello/", &helloHandler)
匹配的路由后面也有/
,那么只会与完全相同的/hello/
匹配,而/hello/other
就会匹配不到,返回错误。
其他的处理器
除net/http
包外,还有如HttpRouter
实现的服务器,可以使用变量实现URL模式匹配,有更多丰富的功能。
4. 请求的处理和响应
请求和响应
Request结构
type Request struct {
Method string
URL *url.URL
Header Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error) // 实例化时通过匿名函数实现 GetBody
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
TLS *tls.ConnectionState
Response *Response
ctx context.Context
...
}
URL结构
type URL struct {
Scheme string
Opaque string // encoded opaque data
User *Userinfo // username and password information
Host string // host or host:port
Path string // path (relative paths may omit leading slash)
RawPath string // encoded path hint (see EscapedPath method)
ForceQuery bool // append a query ('?') even if RawQuery is empty
RawQuery string // encoded query values, without '?'
Fragment string // fragment for references, without '#'
RawFragment string // encoded fragment hint (see EscapedFragment method)
}
请求首部
一个请求的Demo:
GET / HTTP/1.1
Host: hackr.jp
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:13.0) Gecko/20100101 Firefox/13.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*; q=0.8
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate DNT: 1
Connection: keep-alive
If-Modified-Since: Fri, 31 Aug 2007 02:02:20 GMT
If-None-Match: "45bae1-16a-46d776ac"
Cache-Control: max-age=0
Go拿到的是这样的:
// http.Request
&{GET /body HTTP/1.1 1 1 map[Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9] Accept-Encoding:[gzip, deflate, br] Accept-Language:[zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6] Cache-Control:[max-age=0] Connection:[keep-alive] Cookie:[csrftoken=AFpbq4QNxdr35oU4SgESrmHLGtgbjLIUrDIkod4ZjsQIq1hynjiMRYphYluqomNE] Sec-Ch-Ua:["Chromium";v="94", "Microsoft Edge";v="94", ";Not A Brand";v="99"] Sec-Ch-Ua-Mobile:[?0] Sec-Ch-Ua-Platform:["Windows"] Sec-Fetch-Dest:[document] Sec-Fetch-Mode:[navigate] Sec-Fetch-Site:[none] Sec-Fetch-User:[?1] Upgrade-Insecure-Requests:[1] User-Agent:[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.47]] {} <nil> 0 [] false 127.0.0.1:8080 map[] map[] <nil> map[] 127.0.0.1:64798 /body <nil> <nil> <nil> 0xc0000f42c0}
请求体是一个map
结构的数据,比如:
map[
Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9]
Accept-Encoding:[gzip, deflate, br]
Accept-Language:[zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6]
Cache-Control:[max-age=0]
Connection:[keep-alive]
Cookie:[csrftoken=AFpbq4uy3rn35oU4SkESrmHLGwvqjLIjuDIkod4ZjsQIq1uknHiMRYphYluqomNE]
....
Upgrade-Insecure-Requests:[1]
User-Agent:[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.47]
]
可以通过:
r.Header["key"]
// or
r.Header.Get("key")
拿到请求首部数据。
请求主体
**一般的,GET请求没有Body内容,只有使用POST方式提交的请求才有Body。**而GET方式传递的数据放在URL中以键值对的形式进行传递。
这是一个POST请求的Demo:
请求中用户发送的数据是服务端所需要的。一般的,这些数据都是从表单中获取的。
从表单中获取数据
需要注意的是,HTML表单的数据类型决定了POST在发送键值对时使用什么样的格式。
值 | 描述 |
---|---|
application/x-www-form-urlencoded | 默认的,浏览器在发送前将所有提交的数据编码成长字符串,不同的键值对用“&”隔开,如:first_name=li&last_name=duo。这种编码更加简单和高效。 |
multipart/form-data | 表单的数据转换成MIME报文。在使用包含文件上传控件的表单时,必须使用该值。 |
text/plain | 空格转换为 “+” 加号,但不对特殊字符编码。 |
设r
是一个http.Request
,获取数据的方式有:
关于文件
从Form中拿到文件需要r.ParseMultipartForm
和r.MultipartForm.File
来获取。
服务器做出响应
服务器给客户端返回响应使用的是http.ResponseWriter
,在创建响应时使用了http.response
。
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
// http.response 是不可导出的
type response struct {
conn *conn
req *Request
reqBody io.ReadCloser
w *bufio.Writer
handlerHeader Header
calledHeader bool
written int64
contentLength int64
status int
...
}
这里有一个值得注意的地方:ServerHTTP中,为什么http.ResponseWriter
传值,而http.Request
是以指针引用方式传递的呢?
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
首先,*http.Request
使用指针传递引用是因为:服务器需要察觉我们对Request
结构的修改,所以必须要传引用。那服务器不用察觉Response
的修改吗?也用。
所以,在http.ResponseWriter
接口的实现response
中,response
是以引用传递的。可以理解为,http.ResponseWriter
中是*response
。所以实际上,传递的还是引用。
func (w *response) Header() Header {
if w.cw.header == nil && w.wroteHeader && !w.cw.wroteHeader {
// Accessing the header between logically writing it
// and physically writing it means we need to allocate
// a clone to snapshot the logically written state.
w.cw.header = w.handlerHeader.Clone()
}
w.calledHeader = true
return w.handlerHeader
}
可以看到,response
在实现http.ResponseWriter
接口时,是以引用的方式。
有多种方法可以对ResponseWriter
进行写入:
// 使用 write 方法将数据写入 http Respons body中
func write(w http.ResponseWriter, r *http.Request) {
str := "info"
w.Write([]byte(str))
}
// 对 Response header 进行设置 如状态码等信息
// @Notice: WriteHeader 执行完毕后 不允许再写入信息了
func header(w http.ResponseWriter, r *http.Request) {
// 设置 Location 实现 重定向页面
w.Header().Set("Location", "https://lizonglin313.github.io/")
w.Header().Set("A", "aaa")
w.WriteHeader(302) // 写入状态码
}
// 向响应中写入 JSON 格式的数据
// @Notice: 要首先 使用 Header 将相应的内容 设置为 JSON 格式
func outputJSON(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
post := &Post{ // Post 是一个实现打好 JSON Tag的结构体
User: "lzl",
Threads: []string{"1", "2", "3"},
}
jsonByte, _ := json.Marshal(post)
w.Write(jsonByte)
}
Cookie
cookie常用于客户端持久化场景中,当客户端向服务器发出一个HTTP请求时,cookie会随着一起被发到服务器。
type Cookie struct {
Name string
Value string
Path string // optional
Domain string // optional
Expires time.Time // optional
RawExpires string // for reading cookies only
// MaxAge=0 表示cookie在浏览器关闭后失效
// MaxAge<0 意味着cookie已经过期,客户端浏览器需要立马移除cookie
// MaxAge>0 意味cookie可以存活多少秒
MaxAge int
Secure bool
HttpOnly bool
SameSite SameSite
Raw string
Unparsed []string // Raw text of unparsed attribute-value pairs
}
一般的,没有设置Expires
的cookie成为会话cookie,这种cookie在浏览器关闭时被自动移除;相反的,设置了的cookie称为持久cookie。Expires
明确指明cookie在什么时间点过期,而MaxAge
则表示过多久cookie会过期。
向浏览器设置cookie
c1 = http.Cookie{...}
c2 = http.Cookie{...}
// 方法一:向Header中设置,注意第二个cookie需要你使用 Add 追加
w.Header().Set("Set-Cookie", c1.String())
w.Header().Add("Set-Cookie", c2.String())
// 方法二:使用http.SetCookie() 方法直接设置
// 这种设置多个cookie时就不需要 Add 了
http.SetCookie(w, &c1)
从浏览器获取cookie
// 方法一:直接在Header拿
cookie := r.Header["Cookie"]
// 方法二:使用 Cookie("key") 方法,只能拿到cookie列表中的键值为 key 的cookie值
cookie, err := r.Cookie("first_cookie") // r.Cookie 返回一个 cookie 指针
// 方法三:使用 Cookies() 方法拿到cookie列表,获得所有cookie
cs := r.Cookies() // r.Cookies 返回一个 cookie 的指针切片
闪现消息
上文中提到:cookie的MaxAge<0
意味着cookie已经过期,客户端浏览器需要立马移除cookie。通过这个性质可以实现类似弹出框的闪现消息。
// 将 某个 cookie 的生存时间 置为 负
// 当再次请求这个处理器后,将找不到 cookie key
func showMessage(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("key")
if err != nil {
if err == http.ErrNoCookie {
fmt.Fprintln(w, "No message found!")
}
} else {
rc := http.Cookie{
Name: "key",
MaxAge: -1,
Expires: time.Unix(1, 0),
}
http.SetCookie(w, &rc)
fmt.Fprintln(w, `If you refresh the page, you'll see "No message found!"`)
}
}
5. 内容展示
Go的模板引擎通过将数据和模板组合在一起,生成最终的HTML返回给浏览器。通常模板引擎有两种类型:
- 无逻辑模板引擎:在模板中使用占位符,在填充数据时仅仅使用字符串替换。
- 嵌入逻辑的模板引擎:在模板中还会有逻辑代码,更灵活,更强大,能处理数据逻辑。
Go的模板引擎是介于无逻辑模板引擎和嵌入逻辑模板引擎之间的一种模板引擎。分别在text/template
中和html/template
中,来处理任意格式的文本数据和HTML格式。
使用Go的Web模板需要两个步骤:
- 对文本格式的模板进行语法分析,创建一个经过语法分析的模板结构。
- 将
ResponseWriter
和模板所需的动态数据传递给模板引擎,生成HTML,然后再返回给ResponseWriter
。
例如:
func process(w http.ResponseWriter, r *http.Request) {
// 模板文件这里需要写路径,而且路径最前面不加 /
t, _ := template.ParseFiles("go_web_Sau/day2/template/templates/temp1.html")
// 生成数据
data := CreateData()
// 将数据写到模板中
t.Execute(w, data)
}
模板的语法分析
上面的代码中,执行了:
t, _ := template.ParseFiles("filepath")
这实际上执行了两个动作,首先使用文件创建出一个新模板,然后再解析它:
t := template.New("filename")
t, _ := t.ParseFiles("filepath")
使用这种两步的方法需要注意的是:
t := template.New("temp6.html").Funcs(funcMap)
t, _ = t.ParseFiles("go_web_Sau/day2/template/templates/temp6.html")
New()
函数后面,只能跟模板名字,而ParseFiles()
中需要的是从项目根路径开始的一个路径。
其中,ParseFiles()
可以接收多个文件名,但是这种情况下只会返回第一个文件的模板,至于其他的模板会放到一个映射里。
Go的模板中,可以包含下面几种动作:
- 条件动作,对传入的数据做条件判断
- 迭代动作,对数据进行遍历
- 设置动作,在模板中对数据赋值
- 包含动作,模板可以相互调用
此外,在模板中还可以使用:
- 管道
- 为模板绑定函数
- 对于不同的如HTML语法、text格式进行上下文感知,例如对将Javascript脚本转译成可读字符串从而避免XSS攻击
更多关于Go template的内容:
Go语言标准库之http/template | 李文周的博客 (liwenzhou.com)
本部分内容的源码Demo-AdvancedGo/go_web_Sau/day2/template at main · lizonglin313/AdvancedGo (github.com)
6. 存储数据
本书的存储模板主要是:
- 内存存储
- CSV存储
- 使用
gob
包进行二进制存储 - 使用数据库和数据库关系映射器
一般的,Web应用对于数据的存储有三种:
- 将数据暂存到内存中
- 生成数据文件,将数据存入文件中
- 使用数据库进行持久化存储
内存存储
内存存储不需要访问磁盘,所以一般来说速度是最快的。但是内存存储的数据不是持久化的,在程序关闭后数据就会丢失。
使用内存存储无非就是用Go的数据结构,如Array、Slice、Map等结构,也可使是扩展的Stack、Queue、Tree等。
在使用内存存储,尤其涉及到数据修改的时候,要着重注意是传递指针引用还是直接传值。
CSV(comma-separated value)存储
也就是逗号分隔符存储,像这样:
使用CSV进行存储时,需要把数据转化成字符串:
writer := csv.NewWriter(csvFile)
for _, post := range allPosts {
line := []string{strconv.Itoa(post.Id), post.Content, post.Author} // 转换成字符串的切片
err := writer.Write(line) // 实际上写入一个buffer
if err != nil {
panic(err)
}
}
writer.Flush() // 最后再将buffer写入文件
encoding/gob
这个包用于管理gob组成的流,这是一种在编码器和解码器之间进行交流的一种二进制数据。
func store(data interface{}, filename string) {
// 创建 缓冲区
buffer := new(bytes.Buffer)
// 创建 编码器 向缓冲区写
encoder := gob.NewEncoder(buffer)
// 编码数据 写道 缓冲区 data -> buffer
err := encoder.Encode(data)
if err != nil {
panic(err)
}
// 将 缓冲区数据 写到 文件
err = ioutil.WriteFile(filename, buffer.Bytes(), 0600)
if err != nil {
panic(err)
}
}
func load(data interface{}, filename string) {
// 从 文件中 读取原始数据
raw, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
// 创建缓冲区 将原始数据写到缓冲区 raw -> buffer
buffer := bytes.NewBuffer(raw)
// 为 缓冲区 创建 解码器
dec := gob.NewDecoder(buffer)
err = dec.Decode(data)
if err != nil {
panic(err)
}
}
数据库与关系映射器ORM
Go对能见到的常用的数据库几乎都支持,并且有Gorm支持。
Gorm允许定义关系、实施数据迁移、串联多个查询以及执行其他的很多高级操作,还可以设置回调函数等。
GORM中文文档:
GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
7. 发挥Go的并发优势
这部分内容会更新到我之前写的Go并发的博文中。