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。

1
r := mux.NewRouter()

注册一个请求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服务器的路由

现在你还疑惑nilhttpListenAndServer(":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 中间件