Go是一门开源程序语言,其目的是为了编译简单、快速和可靠软件。看看这里有哪些著名的公司使用go打造他们的服务器。Go Web Examples提供了如何使用go程序语言做web开发的简单易懂代码片段。这受到Go By Example的启发,Go By Example 很好地介绍了go这门语言的基础。如果你正在学习go web开发或刚刚开始,在这里你可以找到很好的例子和教程。我们的目标是提供简洁例子和详细细节,这样你可以成为下一个go web开发者。Go Web Examples涵盖了web编程的基础,从路由和模版到中间件和websockets。这里你可以找到简洁的代码片段和详细教程。原文
Hello World
Go是一门内置webserver的编程语言,其标准库中的net/http
包涵盖了有关HTTP协议的所有功能。这当中包含HTTP客户端和HTTP服务端。在这个例子中,你会发现建立一个你可以在浏览器浏览的webserver是多么简单。
Registering a request handler
第一,创建一个用于接受来自浏览器、HTTP客户端或API请求的HTTP连接的Handler。一个Handler在Go中是一个带有签名的函数:
1
|
func (w http.ResponseWriter, r *http.Request)
|
这个函数接收两个参数:
- 一个
http.ResposeWriter
,在这里你可以写响应的文字或html
- 一个
http.Request
, 这里包含了有关HTTP请求的所有信息,例如URL或头字段
向默认的HTTP服务注册一个请求Handler如下一样简单:
1
2
3
|
http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
})
|
Listen for HTTP Connections
请求handler不可以独自接受来自外面的任何HTTP连接,必须有一个HTTP服务器监听一个端口将连接传给请求handler。下面的代码段可以启动GO默认HTTP服务并监听80端口的连接。
1
|
http.ListenAndServe(":80", nil)
|
The Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
})
http.ListenAndServe(":80", nil)
}
|
HTTP Server
在这个例子你可以学会使用Go创建一个基本的HTTP服务器。首先,让我们先来谈谈HTTP服务应该具备的功能。一个基本的HTTP服务需要关注几个关键功能。
- 处理动态请求:来自用户浏览网站、登陆账号或发送图片的动态请求。
- 提供静态资源服务:提供浏览器创建给用户动态体验的javascript,css和图片
- 接受连接: 此HTTP服务器必须监听一个可个让互联网连接的特定端口
处理动态请求(Processing dynamic requests)
net/http
包有用于接受请求和动态处理它们的功能。我们可以用http.HandleFunc
函数注册一个新的handler。它的第一个参数匹配的路径和第二个是执行函数。在这个例子中,当有人浏览你的网站时,他收到一个问候。
1
2
3
|
http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Welcome to my website!")
})
|
在动态方面,http.Request
包含了请求的所有信息和它的参数,你可以用r.URL.Query().Get("token")
读取Get请求参数,或使用r.FormValue("email")
读取来自表单的Post。
提供静态资源服务(Serving static assets)
为了提供像Javascript、CSS 和图片那样的静态资源,我们内置一个指向一个URL路径的http.FileServer
。为了文件服务器更好地工作,它需要知道服务文件从哪里来。我们可以像这样做:
1
|
fs := http.FileServer(http.Dir("static/"))
|
一旦我们文件服务器在那里,我们仅仅需要给它指向一个URL路径,就像我们的动态请求一样。有一点需要注意:为了文件服务器能(按我们的思路)正确地运行,我们需要去掉一部分URL。这个通常是我们文件所在目录名。»
1
|
http.Handle("/static/", http.StripPrefix("/static/", fs))
|
接受请求(Accept connections)
完成我们基本HTTP 服务器最后一步:监听一个端口去接受来自互联网的请求。你可以猜到Go也内置了一个HTTP服务器,我们可以开始毫不费力快速启动。一旦启动,你可以通过浏览器查看你的HTTP服务器了。»
1
|
http.ListenAndServe(":80", nil)
|
The Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to my website!")
})
fs := http.FileServer(http.Dir("static/"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
http.ListenAndServe(":80", nil)
}
|
Routing(using gorilla/mux)
GO的net/http
包提供了HTTP协议的许多功能,但有一件它做不好的事是复杂的路由请求,如将请求URL分段成单个参数。幸运的是有一个非常著名的包可以做这事,这个包因在Go社区高质量的代码而闻名。在这个例子中你将明白如何使用gorilla/mux
包去创建带有参数名、GET/POST handlers和域名限制的路由。
安装gorilla/mux包
gorilla/mux
是一个适配GO默认HTTP路由的包。在编写web应用的时候,它带来了可以提高生产效率的许多特性。它还兼容了GO默认请求handler签名函数func(w http.ResponseWriter, r *http.Request)
,所以可以混搭和切合其他HTTP包(如中间件或现有的应用).
1
|
go get -u github.com/gorilla/mux
|
新建一个路由
首先创建一个请求路由。这个是你web应用的主要路由,它会通过参数形式传给服务器。这个路由会接收所有HTTP连接并将此传给你将要注册在上面的请求handlers。
注册一个请求handler
一旦你新建了一个路由,你就可以像以往在上面注册handlers。唯一的不同是:不是调用http.HandleFunc(...)
,而是使用r.HandlerFunc(...)
URL参数
gorilla/mux
最强大的地方是可以从请求URL中取出某一部分。举个例子,下面是你应用的一个URL
1
|
/books/go-programming-blueprint/page/10
|
这个URL有两个动态部分
为了让请求handler匹配上面提到的使用占位符替代动态部分的URL,你可以这样做:
1
2
3
4
|
r.HandleFunc("/books/{title}/page/{page}", func(w http.ResponseWriter, r *http.Request) {
// get the book
// navigate to the page
})
|
最后是从占位符中取出数据。这个包用 mux.Vars(r)
函数将http.Request
转换成参数并返回一个map
1
2
3
4
5
|
func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
vars["title"] // the book title slug
vars["page"] // the page
}
|
设置HTTP服务器的路由
现在你还疑惑nil
在httpListenAndServer(":80",nil)
意思吗?它是HTTP服务器的主要路由参数。默认是nill
,表示使用net/http
包默认路由。为了保证是有你自己的路由,将nil
替代为你的路由变量r
1
|
http.ListenAndServe(":80", r)
|
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/books/{title}/page/{page}", func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
title := vars["title"]
page := vars["page"]
fmt.Fprintf(w, "You've requested the book: %s on page %s\n", title, page)
})
http.ListenAndServe(":80", r)
}
|
gorilla/mux
的特性
Methods
限制请求handler特定HTTP方式
1
2
3
4
|
r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")
|
Hostnames & Subdomains
限制请求handler去处理http/https。
1
2
|
r.HandleFunc("/secure", SecureHandler).Schemes("https")
r.HandleFunc("/insecure", InsecureHandler).Schemes("http")
|
Path Prefixes & Subrouter
设置请求handler路径前缀
1
2
3
|
bookrouter := r.PathPrefix("/books").Subrouter()
bookrouter.HandleFunc("/", AllBooks)
bookrouter.HandleFunc("/{title}", GetBook)
|
Templates
GO的html/template
包为HTML模版提供了丰富的模版语言。在web应用中几乎都是用它以一个结构体形式将数据传给客户端浏览器。GO的模版语言一大好处是自动转义数据。在Go解析HTML模版时,不需要担心XSS攻击将所有的输入在显示到浏览器之前将其转义。
第一个模版
用GO写一个模版是非常简单的。这个例子显示了一个TODO列表,将其写成无序HTML列表。当渲染模版时,这个数据将以GO任何数据结构传递。这个数据可能是字符串或一个数字,甚至是像下面例子的嵌套数据结构。为了在模版中获取数据,顶层变量必须通过{{.}}
获得。花括号里的原点称为管道和根元素数据。
1
2
3
4
5
6
7
8
9
|
data := TodoPageData{
PageTitle: "My TODO list",
Todos: []Todo{
{Title: "Task 1", Done: false},
{Title: "Task 2", Done: true},
{Title: "Task 3", Done: true},
},
}
|
1
2
3
4
5
6
7
8
9
10
|
<h1>{{.PageTitle}}<h1>
<ul>
{{range .Todos}}
{{if .Done}}
<li class="done">{{.Title}}</li>
{{else}}
<li>{{.Title}}</li>
{{end}}
{{end}}
</ul>
|
控制结构
GO模版语言包含了丰富的控制结构集去渲染你的HTML。这里你将看到最常用的控制结构。获得详细的所欲结构列表请访问»
Control Structure |
Definition |
{{/* a comment */}} |
定义一个注释 |
{{.}} |
获取根元素 |
{{.Title}} |
在一个嵌套元素中获取Title数据成员 |
{{if .Done}} {{else}} {{end}} |
定义一个if |
{{range .Todos}} {{.}} {{.end}} |
循环所有“Todos” 并通过使用{{.}} 获取每一个渲染数据打 |
{{block "content" .}} {{end}} |
定义一个名为“content”的block |
从文件中解析模版
模版可以冲一个字符串或磁盘上的文件解析。比较常用的方法是冲磁盘解析模版,这个例子就是显示如何做。在这个例子中有一个名为layout.html
模版文件在GO项目相同的目录下。
1
2
3
|
tmpl, err := template.ParseFiles("layout.html")
// or
tmpl := template.Must(template.ParseFiles("layout.html"))
|
在请求handler中执行模版
一旦模版已从磁盘解析,它将要被用在请求handler中。Execute
函数接受一个io.Writer
写 出模版和通过interface{}
将数据传给模版。当这个函数调用http.ResponseWriter
时,Content-Type自动以Content-Type: text/html; charset=utf-8
响应。
1
2
3
|
func(w http.ResponseWriter, r *http.Request) {
tmpl.Execute(w, "data goes here")
}
|
代码
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
|
package main
import (
"html/template"
"net/http"
)
type Todo struct {
Title string
Done bool
}
type TodoPageData struct {
PageTitle string
Todos []Todo
}
func main() {
tmpl := template.Must(template.ParseFiles("layout.html"))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := TodoPageData{
PageTitle: "My TODO list",
Todos: []Todo{
{Title: "Task 1", Done: false},
{Title: "Task 2", Done: true},
{Title: "Task 3", Done: true},
},
}
tmpl.Execute(w, data)
})
http.ListenAndServe(":80", nil)
}
|
1
2
3
4
5
6
7
8
9
10
|
<h1>{{.PageTitle}}<h1>
<ul>
{{range .Todos}}
{{if .Done}}
<li class="done">{{.Title}}</li>
{{else}}
<li>{{.Title}}</li>
{{end}}
{{end}}
</ul>
|
Requests & Froms
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
|
// forms.go
package main
import (
"html/template"
"net/http"
)
type ContactDetails struct {
Email string
Subject string
Message string
}
func main() {
tmpl := template.Must(template.ParseFiles("forms.html"))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
tmpl.Execute(w, nil)
return
}
details := ContactDetails{
Email: r.FormValue("email"),
Subject: r.FormValue("subject"),
Message: r.FormValue("message"),
}
// do something with details
_ = details
tmpl.Execute(w, struct{ Success bool }{true})
})
http.ListenAndServe(":8080", nil)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<!-- forms.html -->
{{if .Success}}
<h1>Thanks for your message!</h1>
{{else}}
<h1>Contact</h1>
<form method="POST">
<label>Email:</label><br />
<input type="text" name="email"><br />
<label>Subject:</label><br />
<input type="text" name="subject"><br />
<label>Message:</label><br />
<textarea name="message"></textarea><br />
<input type="submit">
</form>
{{end}}
|
Assets & Files
这个例子将显示如何从一个特定的路径获得像CSS、javascript、图片静态文件。
1
2
3
4
5
6
7
8
9
10
11
|
// static-files.go
package main
import "net/http"
func main() {
fs := http.FileServer(http.Dir("assets/"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
http.ListenAndServe(":8080", nil)
}
|
1
2
3
4
|
$ tree assets/
assets/
└── css
└── styles.css
|
1
2
3
4
5
6
7
|
$ go run static-files.go
$ curl -s http://localhost:8080/static/css/styles.css
body {
background-color: black;
}
|
中间件(Middleware)
一个简单中间件
这个例子将显示在GO如何新建一个基本的中间件。这个中间件简单地将http.HandlerFunc
作为它的一个参数,包裹它并返回一个新的http.HandlerFunc
给服务器调用。
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
|
// basic-middleware.go
package main
import (
"fmt"
"log"
"net/http"
)
func logging(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(r.URL.Path)
f(w, r)
}
}
func foo(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "foo")
}
func bar(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "bar")
}
func main() {
http.HandleFunc("/foo", logging(foo))
http.HandleFunc("/bar", logging(bar))
http.ListenAndServe(":8080", nil)
}
|
1
2
3
4
5
6
7
8
|
$ go run basic-middleware.go
2017/02/10 23:59:34 /foo
2017/02/10 23:59:35 /bar
2017/02/10 23:59:36 /foo?bar
$ curl -s http://localhost:8080/foo
$ curl -s http://localhost:8080/bar
$ curl -s http://localhost:8080/foo?bar
|
高级版中间件
这个例子将显示在GO中如何新建一个更高级版本的中间件。这里我们定义一个新类型Middleware
,这个Middleware最终可以轻易地将多个中间件串联在一起。这个想法的灵感来自Mat Ryer’s关于Building APIs的演讲。你可以在这里找到这个演讲的详细解释。
此段代码详细说明了一个新的中间件如何创建。在下面这个完整的例子中,我们通过一些样板代码来减少此版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
func createNewMiddleware() Middleware {
// Create a new Middleware
middleware := func(next http.HandlerFunc) http.HandlerFunc {
// Define the http.HandlerFunc which is called by the server eventually
handler := func(w http.ResponseWriter, r *http.Request) {
// ... do middleware things
// Call the next middleware/handler in chain
next(w, r)
}
// Return newly created handler
return handler
}
// Return newly created middleware
return middleware
}
|
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
// advanced-middleware.go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
type Middleware func(http.HandlerFunc) http.HandlerFunc
// Logging logs all requests with its path and the time it took to process
func Logging() Middleware {
// Create a new Middleware
return func(f http.HandlerFunc) http.HandlerFunc {
// Define the http.HandlerFunc
return func(w http.ResponseWriter, r *http.Request) {
// Do middleware things
start := time.Now()
defer func() { log.Println(r.URL.Path, time.Since(start)) }()
// Call the next middleware/handler in chain
f(w, r)
}
}
}
// Method ensures that url can only be requested with a specific method, else returns a 400 Bad Request
func Method(m string) Middleware {
// Create a new Middleware
return func(f http.HandlerFunc) http.HandlerFunc {
// Define the http.HandlerFunc
return func(w http.ResponseWriter, r *http.Request) {
// Do middleware things
if r.Method != m {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// Call the next middleware/handler in chain
f(w, r)
}
}
}
// Chain applies middlewares to a http.HandlerFunc
func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
for _, m := range middlewares {
f = m(f)
}
return f
}
func Hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world")
}
func main() {
http.HandleFunc("/", Chain(Hello, Method("GET"), Logging()))
http.ListenAndServe(":8080", nil)
}
|
1
2
3
4
5
6
7
8
9
|
$ go run advanced-middleware.go
2017/02/11 00:34:53 / 0s
$ curl -s http://localhost:8080/
hello world
$ curl -s -XPOST http://localhost:8080/
Bad Request
|
Session
这个例子将显示如何使用GO著名包gorilla/sessions将数据存储在session cookies 中。
cookies是一个用户存储在浏览器的小块数据,它们在每次请求中发送被发送给服务器。在这些cookies中,我们可以存储例如用户是否登陆了我们网站并且判断它(在我们系统中)实际是谁?在这个例子中,我们仅允许已授权用户查看我们在/secret
页中的秘密信息。为了可以访问它,第一步必须先访问/login
以获得一个有效的记录他的session cookie。此外,他可以访问/logout
以废除获取我们秘密信息的权限。
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
53
54
55
|
// sessions.go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/sessions"
)
var (
// key must be 16, 24 or 32 bytes long (AES-128, AES-192 or AES-256)
key = []byte("super-secret-key")
store = sessions.NewCookieStore(key)
)
func secret(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "cookie-name")
// Check if user is authenticated
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// Print secret message
fmt.Fprintln(w, "The cake is a lie!")
}
func login(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "cookie-name")
// Authentication goes here
// ...
// Set user as authenticated
session.Values["authenticated"] = true
session.Save(r, w)
}
func logout(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "cookie-name")
// Revoke users authentication
session.Values["authenticated"] = false
session.Save(r, w)
}
func main() {
http.HandleFunc("/secret", secret)
http.HandleFunc("/login", login)
http.HandleFunc("/logout", logout)
http.ListenAndServe(":8080", nil)
}
|
JSON
这个例子显示如何使用encoding/json
包编码和解码 JSON 数据。
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
|
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type User struct {
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Age int `json:"age"`
}
func main() {
http.HandleFunc("/decode", func(w http.ResponseWriter, r *http.Request) {
var user User
json.NewDecoder(r.Body).Decode(&user)
fmt.Fprintf(w, "%s %s is %d years old!", user.Firstname, user.Lastname, user.Age)
})
http.HandleFunc("/encode", func(w http.ResponseWriter, r *http.Request) {
peter := User{
Firstname: "John",
Lastname: "Doe",
Age: 25,
}
json.NewEncoder(w).Encode(peter)
})
http.ListenAndServe(":8080", nil)
}
|
Websockets
这个例子将说明如何在GO中使用websocket。我们将建立一个简单服务器,这个服务器会回复所有我们发给它的信息。为此我们需要go get
一个著名的库 gorilla/websocket:
1
|
$ go get github.com/gorilla/websocket
|
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
|
// websockets.go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func main() {
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil) // error ignored for sake of simplicity
for {
// Read message from browser
msgType, msg, err := conn.ReadMessage()
if err != nil {
return
}
// Print the message to the console
fmt.Printf("%s sent: %s\n", conn.RemoteAddr(), string(msg))
// Write message back to browser
if err = conn.WriteMessage(msgType, msg); err != nil {
return
}
}
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "websockets.html")
})
http.ListenAndServe(":8080", nil)
}
|
Password Hashing(bcrypt)
这个例子将说明如何使用bcrypt散列密码。为此我们需要go get
golang bcrypt 库:
1
|
$ go get golang.org/x/crypto/bcrypt
|
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
|
// passwords.go
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func main() {
password := "secret"
hash, _ := HashPassword(password) // ignore error for the sake of simplicity
fmt.Println("Password:", password)
fmt.Println("Hash: ", hash)
match := CheckPasswordHash(password, hash)
fmt.Println("Match: ", match)
}
|
参考
原文
middleware 中间件