# 19.4 用户界面：web 服务端

（本节代码见 [goto_v1/main.go](examples/chapter_19/goto_v1/main.go)。）

我们尚未编写启动程序的必要函数。它们（总是）类似 C，C++ 或 Java 中的 `main()` 函数，我们的 web 服务器由它启动，例如用如下命令在本地 8080 端口启动 web 服务器：
```go
http.ListenAndServe(":8080", nil)
```

（web 服务器的功能来自于 `http` 包，[15 章](15.0.md) 做了深入介绍）。web 服务器会在一个无限循环中监听到来的请求，但我们必须定义针对这些请求，服务器该如何响应。可以用被称为 HTTP 处理器的 `HandleFunc` 函数来办到，例如代码：
```go
http.HandleFunc("/add", Add)
```
如此，每个以 `/add` 结尾的请求都会调用 `Add` 函数（尚未完成）。

程序有两个 HTTP 处理器：
- `Redirect`，用于对短 URL 重定向
- `Add`，用于处理新提交的 URL

示意图：

![](images/19.4_fig19.1.jpg?raw=true)

最简单的 `main()` 函数类似这样：
```go
func main() {
	http.HandleFunc("/", Redirect)
	http.HandleFunc("/add", Add)
	http.ListenAndServe(":8080", nil)
}
```

对 `/add` 的请求由 `Add` 处理器处理，所有其他请求会被 `Redirect` 处理器处理。处理函数从到来的请求（一个类型为 `*http.Request` 的变量）中获取信息，然后产生响应并写入 `http.ResponseWriter` 类型变量 `w`。

`Add` 函数必须做的事有：
1. 读取长 URL，即：用 `r.FormValue("url")` 从 HTML 表单提交的 HTTP 请求中读取 URL
2. 使用 store 上的 `Put()` 方法存储长 URL
3. 将对应的短 URL 发送给用户

每个需求都转化为一行代码：
```go
func Add(w http.ResponseWriter, r *http.Request) {
	url := r.FormValue("url")
	key := store.Put(url)
	fmt.Fprintf(w, "http://localhost:8080/%s", key)
}
```

这里 `fmt` 包的 `Fprintf()` 函数用来替换字符串中的关键字 `%s`，然后将结果作为响应发送回客户端。注意 `Fprintf()` 把数据写到了 `ResponseWriter` 中，其实 `Fprintf()` 可以将数据写到任何实现了 `io.Writer` 的数据结构，即该结构实现了 `Write()` 方法。Go 中 `io.Writer` 称为接口，可见 `Fprintf()` 利用接口变得十分通用，可以对很多不同的类型写入数据。Go 中接口的使用十分普遍，它使代码更通用（见 [11 章](11.0.md)）。

还需要一个表单，仍然可以用 `Fprintf()` 来输出，这次将常量写入 `w`。让我们来修改 `Add()`，当未指定 URL 时显示 HTML 表单：
```go
func Add(w http.ResponseWriter, r *http.Request) {
	url := r.FormValue("url")
	if url == "" {
		fmt.Fprint(w, AddForm)
		return
	}
	key := store.Put(url)
	fmt.Fprintf(w, "http://localhost:8080/%s", key)
}

const AddForm = `
<form method="POST" action="/add">
URL: <input type="text" name="url">
<input type="submit" value="Add">
</form>
`
```

在那种情况下，发送字符串常量 `AddForm` 到客户端，它是 html 表单，包含一个 `url` 输入域和一个提交按钮，点击后发送 POST 请求到 `/add`。这样 `Add()` 处理函数被再次调用，此时 `url` 的值来自文本域。（` `` ` 用来创建原始字符串，否则按惯例 `""` 将成为字符串边界。）

`Redirect()` 函数在 HTTP 请求路径中找到键（短 URL 的键是请求路径去除首字符，在 Go 中可以写为 `[1:]`。例如请求 "/abc"，键就是 "abc"），用 `Get()` 函数从 `store` 检索到对应的长 URL，对用户发送 HTTP 重定向。如果没找到 URL，发送 404 "Not Found" 错误取而代之：

```go
func Redirect(w http.ResponseWriter, r *http.Request) {
	key := r.URL.Path[1:]
	url := store.Get(key)
	if url == "" {
		http.NotFound(w, r)
		return
	}
	http.Redirect(w, r, url, http.StatusFound)
}
```

（`http.NotFound()` 和 `http.Redirect()` 是发送通用 HTTP 响应的工具函数。）

我们已经完整地遍历了 [goto_v1](examples/chapter_19/goto_v1) 的代码。

## 编译和运行

可执行程序已包含在示例代码下，如果你想立即测试可以跳过本节。其中包含 3 个 go 源文件和一个 Makefile 文件，通过它应用可以被编译和链接，只须如下操作：
- **Linux 和 OSX 平台：** 在终端窗口源码目录下启动 `make` 命令，或在 LiteIDE 中构建项目。
- **Windows 平台：** 启动 MINGW 环境，步骤为：开始菜单，所有程序，MinGW，MinGW Shell（见 [2.5.5 节](02.5.md)），在命令行窗口输入 `make` 并回车，源代码被编译并链接为原生 exe 可执行程序。

生成内容为可执行程序，Linux/OS X 下为 `goto`，Windows 下为 `goto.exe`。

要启动并运行 web 服务器，那么：
- **Linux 和 OSX 平台：** 输入命令 `./goto`。
- **Windows 平台：** 从 Go IDE 启动程序（如果 Windows 防火墙阻止程序启动，设置允许该程序）

## 测试该程序

打开浏览器并请求  url：`http://localhost:8080/add`

这会激活 `Add()` 处理函数。请求还未包含 url 变量，所以响应会输出 html 表单询问输入：

![](images/19.4_fig19.2.png?raw=true)

添加一个长 URL 以获取等价的缩短版本，例如 `http://golang.org/pkg/bufio/#Writer`，然后单击按钮。应用会为你产生一个短 URL 并打印出来，例如 `http://
localhost:8080/2`。

![](images/19.4_fig19.3.jpg?raw=true)

复制该 URL 并在浏览器地址栏粘贴以发出请求，现在轮到 `Redirect()` 处理函数上场了，对应长 URL 的页面被显示了出来。

![](images/19.4_fig19.4.jpg?raw=true)

## 链接

- [目录](directory.md)
- 上一节：[数据结构](19.3.md)
- 下一节：[持久化存储：gob](19.5.md)
