# 12.7.2 用Go创建网站

您还记得[第四章](https://github.com/hantmac/Mastering_Go_ZH_CN/tree/master/eBook/chapter4/4.0.md)（组合类型的使用）中的 `keyValue.go` 应用，和[第八章](https://github.com/hantmac/Mastering_Go_ZH_CN/tree/master/eBook/chapter8/8.0.md)（Go UNIX系统编程）中的 `kvSaveLoad.go`吗？在这节，您将学习使用 Go 标准库给它们创建 web 接口。这个新的 Go 源代码文件命名为 `kvWeb.go`，并分为6部分展示。

这个 `kvWeb.go` 和之前的 `www.go` 的第一个不同是，`kvWeb.go` 使用 `http.NewServeMux` 来处理 HTTP 请求，因为对于稍复杂点的 web 应用来说，它有更多的功能。

`kvWeb.go` 的第一部分如下：

```go
package main

import (
    "encoding/gob"
    "fmt"
    "html/template"
    "net/http"
    "os"
)

type myElement struct {
    Name    string
    Surname string
    Id      string
}

var DATA = make(map[string]myElement)
var DATAFILE = "/tmp/dataFile.gob"
```

在[第八章](https://github.com/hantmac/Mastering_Go_ZH_CN/tree/master/eBook/chapter8/8.0.md)（Go UNIX系统编程）中，您已经在 `kvSaveLoad.go` 见过上面的代码了。

`kvWeb.go` 的第二部分代码如下：

```go
func save() error {
    fmt.Println("Saving", DATAFILE)
    err := os.Remove(DATAFILE)
    if err != nil {
        fmt.Println(err)
    }
    saveTo, err := os.Create(DATAFILE)
    if err != nil {
        fmt.Println("Cannot create", DATAFILE)
        return err
    }
    defer saveTo.Close()

    encoder := gob.NewEncoder(saveTo)
    err = encoder.Encode(DATA)
    if err != nil {
        fmt.Println("Cannot save to", DATAFILE)
        return err
    }
    return nil
}

func load() error {
    fmt.Println("Loading", DATAFILE)
    loadFrom, err := os.Open(DATAFILE)
    defer loadFrom.Close()
    if err != nil {
        fmt.Println("Empty key/value store!")
        return err
    }

    decoder := gob.NewDecoder(loadFrom)
    decoder.Decode(&DATA)
    return nil
}

func ADD(k string, n myElement) bool {
    if k == "" {
        return false
    }

    if LOOKUP(k) == nil {
        DATA[k] = n
        return true
    }
    return false
}

func DELETE(k string) bool {
    if LOOKUP(k) != nil {
        delete(DATA, k)
        return true
    }
    return false
}

func LOOKUP(k string) *myElement {
    _, ok := DATA[k]
    if ok {
        n := DATA[k]
        return &n
    } else {
        return nil
    }
}

func CHANGE(k string, n myElement) bool {
    DATA[k] = n
    return true
}

func PRINT() {
    for k, d := range DATA {
        fmt.Println("key: %s value: %v\n", k, d)
    }
}
```

您应该也熟悉上面这段代码，因为它曾第一次出现在[第八章](https://github.com/hantmac/Mastering_Go_ZH_CN/tree/master/eBook/chapter8/8.0.md)的 `kvSaveLoad.go` 中。

`kvWeb.go` 的第三部分如下：

```go
func homePage(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Serving", r.Host, "for", r.URL.Path)
    myT := template.Must(template.ParseGlob("home.gohtml"))
    myT.ExecuteTemplate(w, "home.gohtml", nil)
}

func listAll(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Listing the contents of the KV store!")
    fmt.Fprintf(w, "<a href=\"/\" style=\"margin-right: 20px;\">Home sweet home!</a>")
    fmt.Fprintf(w, "<a href=\"/list\" style=\"margin-right: 20px;\">List all elements!</a>")
    fmt.Fprintf(w, "<a href=\"/change\" style=\"margin-right: 20px;\">Change an elements!</a>")    
    fmt.Fprintf(w, "<a href=\"/insert\" style=\"margin-right: 20px;\">Insert an elements!</a>")    

    fmt.Fprintf(w, "<h1>The contents of the KV store are:</h1>")
    fmt.Fprintf(w, "<ul>")
    for k, v := range DATA {
        fmt.Fprintf(w, "<li>")
        fmt.Fprintf(w, "<strong>%s</strong> with value: %v\n", k, v)
        fmt.Fprintf(w, "</li>")
    }
    fmt.Fprintf(w, "</ul>")
}
```

`listAll()` 函数没有使用任何 Go 模版来生成动态输出，而是使用 Go 动态生成的。您可以把这当作一个例外，因为 web 应用通常使用 HTML 模版和 `html/templates`标准库比较好。

`kvWeb.go`的第四部分包含如下代码：

```go
func changeElement(w http.ResponseWriter, r *http.Request){
    fmt.Println("Change an element of the KV store!")
    tmpl := template.Must(template.ParseFiles("update.gohtml"))
    if r.Method != http.MethodPost {
        tmpl.Execute(w, nil)
        return
    }

    key := r.FormValue("key")
    n := myElement{
        Name:    r.FormValue("name"),
        Surname: r.FormValue("surname"),
        Id:      r.FormValue("id"),
    }

    if !CHANGE(key, n) {
        fmt.Println("Update operation failed!")
    } else {
        err := save()
        if err != nil {
            fmt.Println(err)
            return
        }
        tmpl.Execute(w, struct {Struct bool}{true})
    }
}
```

从上面这段代码，您能看到在 `FormValue()` 函数的帮助下怎么读取一个 HTML 表单中字段的值。这个 `template.Must()` 函数是一个帮助函数，用于确保提供的模版文件不包含错误。

`kvWeb.go` 的第五段代码如下：

```go
func insertElement(w http.ResponseWriter, r *http.Request){
    fmt.Println("Inserting an element to the KV store!")
    tmpl := template.Must(template.ParseFiles("insert.gohtml"))
    if r.Method != http.MethodPost {
        tmpl.Execute(w, nil)
        return
    }

    key := r.FormValue("key")
    n := myElement {
        Name:    r.FormValue("name"),
        Surname: r.FormValue("surname"),
        Id:      r.FormValue("id"),
    }

    if !ADD(key, n) {
        fmt.Println("Add operation failed!")
    } else {
        err := save()
        if err != nil {
            fmt.Println(err)
            return
        }
        tmpl.Execute(w, struct{Success bool}{true})
    }
}
```

余下代码如下：

```go
func main() {
    err := load()
    if err != nil {
        fmt.Println(err)
    }

    PORT := ":8001"
    arguments := os.Args
    if len(arguments) == 1 {
        fmt.Println("Using default port number: ", PORT)
    } else {
        PORT = ":" + arguments[1]
    }

    http.HandleFunc("/", homePage)
    http.HandleFunc("/change", changeElement)
    http.HandleFunc("/list", listAll)
    http.HandleFunc("/insert", insertElement)
    err = http.ListenAndServe(PORT, nil)
    if err != nil {
        fmt.Println(err)
    }
}
```

这个 `kvWeb.go` 的 `main()` 函数要比[第八章](https://github.com/hantmac/Mastering_Go_ZH_CN/tree/master/eBook/chapter8/8.0.md)（Go UNIX系统编程）的 `kvSaveLoad.go` 的 `main()` 函数简单，因为这俩个程序有完全不同的设计。

现在看一下 `gohtml` 文件，这个工程使用的起始文件是如下的 `home.gohtml`：

```markup
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>A Key Value Store!</title>
</head>
<body>

<a href="/" style="margin-right: 20px;">Home sweet home!</a>
<a href="/list" style="margin-right: 20px;">List all elements!</a>
<a href="/change" style="margin-right: 20px;">Change an elements!</a>
<a href="/insert" style="margin-right: 20px;">Insert an elements!</a>

<h2>Welcome to the Go KV store!</h2>
</body>
</html>
```

这个 `home.gohtml` 文件是静态的，就是说它的内容没有改变。而，其他的 `gohtml` 文件是动态显示信息。

`update.gohtml` 的内容如下：

```markup
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>A Key Value Store!</title>
</head>
<body>

<a href="/" style="margin-right: 20px;">Home sweet home!</a>
<a href="/list" style="margin-right: 20px;">List all elements!</a>
<a href="/change" style="margin-right: 20px;">Change an elements!</a>
<a href="/insert" style="margin-right: 20px;">Insert an elements!</a>

{{if .Success}} <h1>Element updated!<h1>{{else}}
<h1>Please fill in the fields:</h1>
    <form method="POST">
        <label>Key:</label><br/>
        <input type="text" name="key"><br/>
        <label>Name:</label><br/>
        <input type="text" name="name"><br/>
        <label>Surname:</label><br/>
        <input type="text" name="surname"><br/>
        <label>Id:</label><br/>
        <input type="text" name="id"><br/>
        <input type="submit">
    </form>
{{end}}

</body>
</html>
```

上面是主要的 HTML 代码。它最有趣的部分是 `if` 声明，它定义了您应该看到表单还是 `Element updated!` 信息。

最后，`insert.gohtml` 的内容如下：

```markup
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>A Key Value Store!</title>
</head>
<body>

<a href="/" style="margin-right: 20px;">Home sweet home!</a>
<a href="/list" style="margin-right: 20px;">List all elements!</a>
<a href="/change" style="margin-right: 20px;">Change an elements!</a>
<a href="/insert" style="margin-right: 20px;">Insert an elements!</a>

{{if .Success}}
    <h1>Element inserted!</h1>
{{else}}
    <h1>Please fill in the fields:</h1>
    <form method="POST">
        <label>Key:</label><br/>
        <input type="text" name="key"><br/>
        <label>Name:</label><br/>
        <input type="text" name="name"><br/>
        <label>Surname:</label><br/>
        <input type="text" name="surname"><br/>
        <label>Id:</label><br/>
        <input type="text" name="id"><br/>
        <input type="submit">
    </form>
{{end}}

</body>
</html>
```

您能从 `<title>` 标签的内容看出，`insert.gohtml` 和 `update.gohtml` 是完全相同的！

在 Unix 命令行中执行 `kvWeb.go` 将产生如下输出：

```
$ go run kvWeb.go
Loading /tmp/dataFile.gob
Using default port number:  :8001
Serving localhost:8001 for /
Serving localhost:8001 for /favicon.ico
Listing the contents of the KV store!
Serving localhost:8001 for /favicon.ico
Inserting an element to the KV store!
Serving localhost:8001 for /favicon.ico
Inserting an element to the KV store!
Add operation failed!
Serving localhost:8001 for /favicon.ico
Inserting an element to the KV store!
Serving localhost:8001 for /favicon.ico
Inserting an element to the KV store!
Saving /tmp/dataFile.gob
Serving localhost:8001 for /favicon.ico
Inserting an element to the KV store!
Serving localhost:8001 for /favicon.ico
Changing an element of the KV store!
Serving localhost:8001 for /vavicon.ico
```

另外，真正有趣的是您可以用 web 浏览器和 `kvWeb.go` 交互。这个网站的首页，定义在 `home.gohtml` 显示截屏如下：

![web 应用的静态首页](https://github.com/hantmac/Mastering_Go_ZH_CN/tree/master/images/chapter12/12.7.2-1.jpg)

下面的截屏显示的是 key-value 存储的内容：

![key-value 存储的内容](https://github.com/hantmac/Mastering_Go_ZH_CN/tree/master/images/chapter12/12.7.2-2.jpg)

下面的截屏显示的是使用 `kvWeb.go` 应用的 web 接口添加新的数据到 key-value 存储中。

![添加一个新数据到 key-value 存储中](https://github.com/hantmac/Mastering_Go_ZH_CN/tree/master/images/chapter12/12.7.2-3.jpg)

下面的截屏显示的是使用 `kvWeb.go` 应用的 web 接口更新已存在 key 数据的值。

![更新 key-value 存储中一个 key 的值](https://github.com/hantmac/Mastering_Go_ZH_CN/tree/master/images/chapter12/12.7.2-4.jpg)

这个 `kvWeb.go` web 应用还不够完美，所以把它作为一个练习尽可能完善它。

> 这节说明了您如何使用 Go 开放整个网址和 web 应用。尽管您的需求无疑是多样的，但是和 `kvWeb.go` 使用的技术是相同的。注意，自己做的网站被认为要比那些由流行的管理系统创建的更安全。
