# Go-Mega

![cover](/files/-LNPxd54XaFf4rvKnUtq)

## Go-Mega

[![License: CC BY-NC-ND 4.0](https://img.shields.io/badge/License-CC%20BY--NC--ND%204.0-brightgreen.svg)](https://raw.githubusercontent.com/bonfy/go-mega/master/LICENSE)

大家好，我是 [BONFY](https://github.com/bonfy) ，拥有多年开发Web的经验，不过想起我当时入门 Python Web 的时候也是看了当时 [Miguel Grinberg](https://blog.miguelgrinberg.com/) 的博客，按照他的教程一步一步搭建最简单的博客，再到自己去研究 **Flask** 源码以及其他的 Web 框架。

目前由于工作需要，我经常要用 **Go** 开发Web应用，也积攒了一些经验和方法，所以就想模仿 [The Flask Mega-Tutorial教程](https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world) 来编写一套 **Go** 的 Web 教程。

本书的目录我尽量和 The Flask Mega 保持一致，不过对于有些章节也会酌情删减。

### 章节总览

| Title                                                     | Source Code                                                                | Zip                                                                |
| --------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [00-Prepare](/00-prepare)                                 |                                                                            |                                                                    |
| [01-Hello-World](/01-hello-world)                         | [View](https://github.com/bonfy/go-mega-code/tree/01-Hello-World)          | [Download](https://github.com/bonfy/go-mega-code/archive/v0.1.zip) |
| [02-Template-Basic](/02-template-basic)                   | [View](https://github.com/bonfy/go-mega-code/tree/02-Template)             | [Download](https://github.com/bonfy/go-mega-code/archive/v0.2.zip) |
| [03-Template-Advance](/03-template-advance)               | [View](https://github.com/bonfy/go-mega-code/tree/03-Template-Advance)     | [Download](https://github.com/bonfy/go-mega-code/archive/v0.3.zip) |
| [04-Web-Form](/04-web-form)                               | [View](https://github.com/bonfy/go-mega-code/tree/04-Web-Form)             | [Download](https://github.com/bonfy/go-mega-code/archive/v0.4.zip) |
| [05-Database](/05-database)                               | [View](https://github.com/bonfy/go-mega-code/tree/05-Database)             | [Download](https://github.com/bonfy/go-mega-code/archive/v0.5.zip) |
| [06-User-Login](/06-user-login)                           | [View](https://github.com/bonfy/go-mega-code/tree/06-User-Login)           | [Download](https://github.com/bonfy/go-mega-code/archive/v0.6.zip) |
| [07-Profile-Page-And-Avatar](/07-profile-page-and-avatar) | [View](https://github.com/bonfy/go-mega-code/tree/07-Profile-Page)         | [Download](https://github.com/bonfy/go-mega-code/archive/v0.7.zip) |
| [08-Follower](/08-follower)                               | [View](https://github.com/bonfy/go-mega-code/tree/08-Follower)             | [Download](https://github.com/bonfy/go-mega-code/archive/v0.8.zip) |
| [09-Pagination](/09-pagination)                           | [View](https://github.com/bonfy/go-mega-code/tree/09-Pagination)           | [Download](https://github.com/bonfy/go-mega-code/archive/v0.9.zip) |
| [10-Email-Support](/10-email-support)                     | [View](https://github.com/bonfy/go-mega-code/tree/10-Email-Support)        | [Download](https://github.com/bonfy/go-mega-code/archive/v1.0.zip) |
| [11-Facelift](/11-facelift)                               | [View](https://github.com/bonfy/go-mega-code/tree/11-Facelift)             | [Download](https://github.com/bonfy/go-mega-code/archive/v1.1.zip) |
| [12-Dates-And-Times](/12-dates-and-times)                 | [View](https://github.com/bonfy/go-mega-code/tree/12-Dates-And-Times)      | [Download](https://github.com/bonfy/go-mega-code/archive/v1.2.zip) |
| [13-Javascript-Magic](/13-javascript-magic)               | [View](https://github.com/bonfy/go-mega-code/tree/13-Javascript-Magic)     | [Download](https://github.com/bonfy/go-mega-code/archive/v1.3.zip) |
| [14-Deployment-On-Heroku](/14-deployment-on-heroku)       | [View](https://github.com/bonfy/go-mega-code/tree/14-Deployment-On-Heroku) | [Download](https://github.com/bonfy/go-mega-code/archive/v1.4.zip) |
| [15-Deployment-On-Linux](/15-deployment-on-linux)         | [View](https://github.com/bonfy/go-mega-code/tree/15-Deployment-On-Linux)  | [Download](https://github.com/bonfy/go-mega-code/archive/v1.5.zip) |
| [16-Summary](/16-summary)                                 |                                                                            |                                                                    |

### 声明

本书由 [BONFY](https://github.com/bonfy) 编写，采用 [CC BY-NC-ND 4.0](http://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh) 协议发布。

这意味着你可以在非商业性使用的前提下自由转载，但必须：

1. 保持署名
2. 不对本书进行修改

### 联系我

如果你对于本书有什么建议或意见，欢迎批评指正，并联系我。

* [个人主页](https://bonfy.im)
* [个人博客](https://blog.bonfy.im)
* [知乎](https://zhuanlan.zhihu.com/bonfy)
* [GitHub](https://github.com/bonfy)
* [Twitter](https://twitter.com/foreverbonfy)
* [Email](mailto:bonfygithub@163.com)

### 相关资源

* [书籍 Github源地址](https://github.com/bonfy/go-mega)
* [源码 Github源地址](https://github.com/bonfy/go-mega-code)
* [Demo 地址](https://go-mega.herokuapp.com)

### 电子书

* [epub 下载链接](https://raw.githubusercontent.com/bonfy/image/master/ebook/Go-Mega%20Tutorial.epub)
* [mobi 下载链接](https://raw.githubusercontent.com/bonfy/image/master/ebook/Go-Mega%20Tutorial.mobi)

### 支持我

如果您感觉有收获，欢迎给我打赏，以激励我输出更多优质内容，感谢支持！

![感谢支持](/files/-LNIhG0Z3FreAiCXBFsn)


# 00-Prepare

## 语言

安装Go,详情见参照Go的官方网站的[安装文档](https://golang.org/doc/install)

这里介绍下Mac下的安装，适用homebrew：

```
$ brew install go

# 安装完成后 查看版本
$ go version
go version go1.11 darwin/amd64

# 查看 go env
$ go env
```

## 编辑器

* Goland (IDE）
* Visual Studio Code （本人推荐，易用，插件易安装）
* VI / Emacs (都是神级编辑器，不过对使用者的要求比较高)

这里推荐使用 Visual Studio Code

```
$ brew cask install visual-studio-code
```

当编写Go代码时，即遇到 `.go`结尾的文件时，它会自动提示你安装插件，十分简便。

## 体会

Go 是一门新生的语言，而且有个好的公司推广（google），而且这几年的势头也是很猛，是一门改良型的C语言

就 Web 编程而言，相对于Python

* 原生 net/http 支持，可以不需要依赖于第三方包
* 底层 goroutine，高并发
* 静态语言编译，高效
* 特殊的 error 处理机制，基本上一次编译成功，后面很少出错
* 编译之后二进制，易于部署

## Links

* [目录](/)
* 下一节: [01-Hello-World](/01-hello-world)


# 01-Hello-World

一般计算机书的开头都是 Hello World

我们亦不能免俗，所以本章我们的任务就是完成最简单的 Hello World

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/01-Hello-World), [Zip](https://github.com/bonfy/go-mega-code/archive/v0.1.zip)

## 建立目录结构

与 Python 相比，Go 对代码存放的位置还是有讲究的，毕竟这是由 Go 特殊的 `package引用机制` 决定的，首先建立自己存放这次代码的文件夹

```
$ cd $GOPATH/src
$ mkdir -p github.com/bonfy/go-mega-code
$ cd github.com/bonfy/go-mega-code
```

这里如果大家有Github账号，而且想上传到自己的repo的话，建议 github.com/your\_user\_name/repo\_name 的文件夹

## Hello World 应用

在 github.com/bonfy/go-mega-code 文件夹下 建立 `main.go`，这是我们程序的主入口

`main.go`

```go
package main

import "net/http"

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World"))
    })
    http.ListenAndServe(":8888", nil)
}
```

短短不到10行代码，我们的 Hello World 应用就已经完成了，而且不需要任何的其他第三方Package，只需要引入官方的 ***net/http*** 就行了，就是这么简单

让我们来运行下面的命令，看下效果

```
$ go run main.go
```

现在打开您的网络浏览器并在地址栏中输入以下URL：

```
http://localhost:8888

或者

http://127.0.0.1:8888
```

![01-01](/files/-LNIc7MaVC_YToKyc8EN)

## 说明

这里对上面的代码进行简单的说明

`func main()` 是主程序入口，主要用到了 ***net/http*** 的两个函数

```go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

func ListenAndServe(addr string, handler Handler) error
```

`HandleFunc` 类似于 flask的 `app.route`, `pattern` 提供了路由路径，handler是一个函数参数，这里我们的程序中传入的是一个匿名函数， 减少了代码

`ListenAndServe` 第一个参数为 addr，如果不提供ip，这里只传入端口，相当于 `0.0.0.0:8888` ，第二个参数 Handler 传入 nil，则表示使用 Default 的 Server

另外 输出 `Hello World` 的办法，大致有三个，如下:

```go
// Case 1: w.Write byte
w.Write([]byte("Hello World"))

// Case 2: fmt.Fprintf
fmt.Fprintf(w, "Hello World")

// Case 3: io.Write
io.WriteString(w, "Hello World")
```

其中第一种用的是 ResponseWriter 的 `Write([]byte) (int, error)` 方法， 而 后面两种是稍微用到了 Go 里面 interface 的特性， ResponseWriter interface 要实现 `Write([]byte) (int, error)` 的方法，所以也就实现了 io.Writer 方法，所以可以作为 io.Writer 的类型作为 后面两个函数的参数。

如果想更深入的了解 net/http 处理请求的话，可以看下Go源码中的 `net/http/server.go`

或者看下 [Go的http包详解](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/03.4.md)

## Links

* [目录](/)
* 上一节: [00-Prepare](/00-prepare)
* 下一节: [02-Template-Basic](/02-template-basic)


# 02-Template-Basic

学习完 [第一章](/01-hello-world) 之后，我们已经拥有了虽然一个简单，但可以成功运行Web应用

本章将沿用这个应用，在此之上，加入模版渲染，使得页面更丰富

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/02-Template), [Diff](https://github.com/bonfy/go-mega-code/compare/01-Hello-World...02-Template), [Zip](https://github.com/bonfy/go-mega-code/archive/v0.2.zip)

## 什么是模板

微博应用程序的主页会有一个欢迎用户的标题。虽然目前的应用程序还没有实现用户概念，但这不妨碍我使用一个 Go Stuct 来模拟一个用户，由于Go是静态语言，先定义 User 的结构，然后再初始化 user ，如下所示：

```go
type User struct {
    Username string
}

user := User{Username: "bonfy"}
```

创建模拟对象是一项实用的技术，它可以让你专注于应用程序的一部分，而无需为系统中尚不存在的其他部分分心。 在设计应用程序主页的时候，我可不希望因为没有一个用户系统来分散我的注意力，因此我使用了模拟用户对象，来继续接下来的工作。

原先的视图函数返回简单的字符串，我现在要将其扩展为包含完整HTML页面元素的字符串，如下所示：

```go
package main

import (
    "html/template"
    "net/http"
)

// User struct
type User struct {
    Username string
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        user := User{Username: "bonfy"}
        tpl, _ := template.New("").Parse(`<html>
            <head>
                <title>Home Page - Bonfy</title>
            </head>
            <body>
                <h1>Hello, {{.Username}}!</h1>
            </body>
        </html>`)
        tpl.Execute(w, &user)
    })
    http.ListenAndServe(":8888", nil)
}
```

对HTML标记语言不熟悉的话，建议阅读一下Wikipedia上的简介[HTML Markup](https://en.wikipedia.org/wiki/HTML#Markup)

利用上述的代码更新这个视图函数，然后再次运行 (这里并没有flask那样子的debug mode，每次更新都必须重新执行)

```
$ go run main.go
```

在浏览器打开它的URL看看结果

![02-01](/files/-LNIc7hH1Y92R4jbGpbS)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/1cf55f62ae8ee34bb9fec64508398480b9168e2c)
>
> Tip: Go 中`反引号`用来创建原生的字符串字面量，这些字符串可能由多行组成(不支持任何转义序列)，原生的字符串字面量多用于书写多行消息、HTML以及正则表达式

我们暂时将 template 以文本方式耦合在代码中，随着我们项目的扩大，这个不利于管理，而且如果公司有前端支持的话，模板的工作很大可能会有前端设计，所以我们有必要对其进行解偶。我们打算将模板文件放入 templates 的文件夹

建立 templates 文件夹

```
$ mkdir templates
```

将模板的内容移到 templates文件夹下

templates/index.html

```markup
<html>
    <head>
        <title>Home Page - Bonfy</title>
    </head>
    <body>
        <h1>Hello, {{.Username}}!</h1>
    </body>
</html>
```

然后将 template 文本从代码中移除

main.go

```go
package main

import (
    "html/template"
    "net/http"
)

// User struct
type User struct {
    Username string
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        user := User{Username: "bonfy"}
        tpl, _ := template.ParseFiles("templates/index.html")
        tpl.Execute(w, &user)
    })
    http.ListenAndServe(":8888", nil)
}
```

然后运行，在浏览器中打开，结果是与刚才一样的，不过代码的组织结构却更清晰了。

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/cca162aa6510f6d8bf96cb21ef8aed5ea0a1a1a1)

## 模板的常用操作

### 条件语句

在 index.html 中加入条件语句

templates/index.html

```markup
<html>
    <head>
        {{if .Title}}
            <title>{{.Title}} - blog</title>
        {{else}}
            <title>Welcome to blog!</title>
        {{end}}
    </head>
    <body>
        <h1>Hello, {{.User.Username}}!</h1>
    </body>
</html>
```

由于 User 没有 Title字段，所以我们引入新的 IndexViewModel 来具体处理所有与View对应的Model

main.go

```go
package main

import (
    "html/template"
    "net/http"
)

// User struct
type User struct {
    Username string
}

// IndexViewModel struct
type IndexViewModel struct {
    Title string
    User  User
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        user := User{Username: "bonfy"}
        v := IndexViewModel{Title: "Homepage", User: user}
        tpl, _ := template.ParseFiles("templates/index.html")
        tpl.Execute(w, &v)
    })
    http.ListenAndServe(":8888", nil)
}
```

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/9df5fb2f1f7d400513e3877aad0704a5e69366ac)

### 循环

在 index.html 中加入循环

```markup
<html>
    <head>
        {{if .Title}}
            <title>{{.Title}} - blog</title>
        {{else}}
            <title>Welcome to blog!</title>
        {{end}}
    </head>
    <body>
        <h1>Hello, {{.User.Username}}!</h1>
        {{range .Posts}}
            <div><p>{{ .User.Username }} says: <b>{{ .Body }}</b></p></div>
        {{end}}
    </body>
</html>
```

在 main.go 的 IndexViewModel 中加入 Post

```go
package main

import (
    "html/template"
    "net/http"
)

// User struct
type User struct {
    Username string
}

// Post struct
type Post struct {
    User User
    Body string
}

// IndexViewModel struct
type IndexViewModel struct {
    Title string
    User  User
    Posts []Post
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        u1 := User{Username: "bonfy"}
        u2 := User{Username: "rene"}

        posts := []Post{
            Post{User: u1, Body: "Beautiful day in Portland!"},
            Post{User: u2, Body: "The Avengers movie was so cool!"},
        }

        v := IndexViewModel{Title: "Homepage", User: u1, Posts: posts}
        tpl, _ := template.ParseFiles("templates/index.html")
        tpl.Execute(w, &v)
    })
    http.ListenAndServe(":8888", nil)
}
```

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/50e5028870d4ab7918d391007162c4387540f165)

运行效果图

![02-02](/files/-LNIc7hjqaEGIEI8PuMe)

## Links

* [目录](/)
* 上一节: [01-Hello-World](/01-hello-world)
* 下一节: [03-Template-Advance](/03-template-advance)


# 03-Template-Advance

学习完 [第二章](/02-template-basic) 之后，你对模板已经有了基本的认识

本章将讨论 Go 的组合特性，以及 建立一个通用的调用 模板的方法

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/03-Template-Advance), [Diff](https://github.com/bonfy/go-mega-code/compare/02-Template...03-Template-Advance), [Zip](https://github.com/bonfy/go-mega-code/archive/v0.3.zip)

## 匿名组合

`匿名组合` 其实是Go里的一个非常重要的特性，在 Go 的世界里没有继承，只有组合（当然还有接口）。组合其实可以实现部分的继承。

main.go

```go
type Post struct {
    User
    Body string
}

// IndexViewModel struct
type IndexViewModel struct {
    Title string
    User
    Posts []Post
}
```

就是 将 `User User -> User`其它都不变，这样执行，发现程序照常运行

不过，现在我们可以改下templates/index.html

templates.index.html

```markup
<html>
    <head>
        {{if .Title}}
            <title>{{.Title}} - blog</title>
        {{else}}
            <title>Welcome to blog!</title>
        {{end}}
    </head>
    <body>
        <h1>Hello, {{.Username}}!</h1>
        {{range .Posts}}
            <div><p>{{ .Username }} says: <b>{{ .Body }}</b></p></div>
        {{end}}

    </body>
</html>
```

由于 `匿名组合` ，我们现在可以将 `{{.User.Username}} -> {{.Username}}`

就是我们可以直接使用 `匿名组合` 的属性，以及方法，其实也是变像的实现了继承。

关于 **Go** 的 面向对象，可以看下 [参考](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/02.5.md)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/134267d02ad558c191387b79afd4b1cb8bcd1622)

## 模板继承

其实 Go 的模板应该没有 Flask jinja2 这样的功能强大，它只有 include，所以为了实现模板的继承，我们需要发挥下主观能动性

main.go

```go
package main

import (
    "html/template"
    "io/ioutil"
    "net/http"
    "os"
)

// User struct
type User struct {
    Username string
}

// Post struct
type Post struct {
    User
    Body string
}

// IndexViewModel struct
type IndexViewModel struct {
    Title string
    User
    Posts []Post
}

// PopulateTemplates func
// Create map template name to template.Template
func PopulateTemplates() map[string]*template.Template {
    const basePath = "templates"
    result := make(map[string]*template.Template)

    layout := template.Must(template.ParseFiles(basePath + "/_base.html"))
    dir, err := os.Open(basePath + "/content")
    if err != nil {
        panic("Failed to open template blocks directory: " + err.Error())
    }
    fis, err := dir.Readdir(-1)
    if err != nil {
        panic("Failed to read contents of content directory: " + err.Error())
    }
    for _, fi := range fis {
        func() {
            f, err := os.Open(basePath + "/content/" + fi.Name())
            if err != nil {
                panic("Failed to open template '" + fi.Name() + "'")
            }
            defer f.Close()
            content, err := ioutil.ReadAll(f)
            if err != nil {
                panic("Failed to read content from file '" + fi.Name() + "'")
            }
            tmpl := template.Must(layout.Clone())
            _, err = tmpl.Parse(string(content))
            if err != nil {
                panic("Failed to parse contents of '" + fi.Name() + "' as template")
            }
            result[fi.Name()] = tmpl
        }()
    }
    return result
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        u1 := User{Username: "bonfy"}
        u2 := User{Username: "rene"}

        posts := []Post{
            Post{User: u1, Body: "Beautiful day in Portland!"},
            Post{User: u2, Body: "The Avengers movie was so cool!"},
        }

        v := IndexViewModel{Title: "Homepage", User: u1, Posts: posts}

        templates := PopulateTemplates()
        templates["index.html"].Execute(w, &v)
    })
    http.ListenAndServe(":8888", nil)
}
```

templates/\_base.html

```markup
<html>
    <head>
        {{if .Title}}
        <title>{{.Title}} - blog</title>
        {{else}}
        <title>Welcome to blog!</title>
        {{end}}
    </head>
    <body>
        <div>Blog: <a href="/">Home</a></div>
        {{template "content" .}}
    </body>
</html>
```

templates/content/index.html

```markup
{{define "content"}}
    <h1>Hello, {{.User.Username}}!</h1>

    {{range .Posts}}
        <div><p>{{ .User.Username }} says: <b>{{ .Body }}</b></p></div>
    {{end}}
{{end}}
```

这里用了模板继承，\_base 是 基础模板，这样 比如 head 等信息不用再重复的去在每个.html文件中重复定义，我们可以专注于每个页面的业务逻辑和内容。

由于没有像 Jinja2 这样的原生支持模板继承，这个实现的关键就是 PopulateTemplates 函数，它的作用是 遍历 templates/content/ 文件夹下的所有文件，并和 templates/\_base.html 合成 template.Template，然后再存入 map 中(在 Python 中一般叫 dict)，可以使用例如 index.html 的 key 来访问。

我们现在运行下程序，页面还是和原来一样(只是我们在 \_base template 里面加入了 Home 的导航)，不过我们的templates文件夹已经有了基础模板，并且具备了快速扩展的能力。下章 [Web Form](/04-web-form) 我们可以看见效果。

![03-01](/files/-LNIhFznBPr_BX0pNM7H)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/e5fd3ef25bcc6874818621d06e68dee3d379778a)

## Links

* [目录](/)
* 上一节: [02-Template-Basic](/02-template-basic)
* 下一节: [04-Web-Form](/04-web-form)


# 04-Web-Form

上两章我们讨论了Template，本章节我们继续讨论 Web 表单

Web表单是所有Web应用程序中最基本的组成部分之一。 本章我们将使用表单来为用户发表动态和登录认证提供途径。

*本章的GitHub链接为* [Source](https://github.com/bonfy/go-mega-code/tree/04-Web-Form), [Diff](https://github.com/bonfy/go-mega-code/compare/03-Template-Advance...04-Web-Form), [Zip](https://github.com/bonfy/go-mega-code/archive/v0.4.zip)

## 结构优化

上一章我们通过 模板继承 以及 PopulateTemplates 对 templates 文件夹进行了梳理，在继续开始Web表单之前，我们再来梳理下其中 **Go** 代码的结构。

目前我们的所有的逻辑都集中在了 `main.go` 文件里，包括 model struct， viewmodel struct 定义，还有 handler 的实现，对于小点的web应用程序，可能这有助于快速开发，不过随着项目的扩大，代码量会变得越来越多，越来越臃肿，最后甚至会影响到阅读代码。这个时候结构的优化就显得尤为重要了。

我们的思路是 建立这样的数据结构:

* package model         - 负责数据建模（以及后一章 [数据库](/05-database) ORM）
* package vm            - 负责View Model
* package controller    - 负责 http 路由

每个文件夹下的 g.go 负责存放该package的全局变量 以及 init 函数。（ 只能说 类似 Python 的 `__init__.py`, 因为 **Go** 其实是通过大小写来表明是否可以外部引用, 不像Python，一定要 import 到 `__init__.py` 文件里才能通过 package 名来引用。)

我们先建立model 文件夹，然后将 Post、User struct 分别移到 `model` 文件夹下

model/user.go

```go
package model

// User struct
type User struct {
    Username string
}
```

model/post.go

```go
package model

// Post struct
type Post struct {
    User
    Body string
}
```

再将 view model 移到 vm文件夹下

vm/g.go

```go
package vm

// BaseViewModel struct
type BaseViewModel struct {
    Title string
}

// SetTitle func
func (v *BaseViewModel) SetTitle(title string) {
    v.Title = title
}
```

由于\_base.html 基础模板中有 Title 字段，所以 Title是每个view都必有的字段，我们将它单独设成个 BaseViewStruct，方便用 `匿名组合`；

vm/index.go

```go
package vm

import "github.com/bonfy/go-mega-code/model"

// IndexViewModel struct
type IndexViewModel struct {
    BaseViewModel
    model.User
    Posts []model.Post
}

// IndexViewModelOp struct
type IndexViewModelOp struct{}

// GetVM func
func (IndexViewModelOp) GetVM() IndexViewModel {
    u1 := model.User{Username: "bonfy"}
    u2 := model.User{Username: "rene"}

    posts := []model.Post{
        model.Post{User: u1, Body: "Beautiful day in Portland!"},
        model.Post{User: u2, Body: "The Avengers movie was so cool!"},
    }

    v := IndexViewModel{BaseViewModel{Title: "Homepage"}, u1, posts}
    return v
}
```

将所有的路由相关移到controller

> `utils.go` 存放 辅助工具函数，一般都是本package引用，所以小写就可以了, 这里PopulateTemplates 函数其实最好是小写，不过不去管它了。

controller/utils.go

```go
package controller

import (
    "html/template"
    "io/ioutil"
    "os"
)

// PopulateTemplates func
// Create map template name to template.Template
func PopulateTemplates() map[string]*template.Template {
    const basePath = "templates"
    result := make(map[string]*template.Template)

    layout := template.Must(template.ParseFiles(basePath + "/_base.html"))
    dir, err := os.Open(basePath + "/content")
    if err != nil {
        panic("Failed to open template blocks directory: " + err.Error())
    }
    fis, err := dir.Readdir(-1)
    if err != nil {
        panic("Failed to read contents of content directory: " + err.Error())
    }
    for _, fi := range fis {
        f, err := os.Open(basePath + "/content/" + fi.Name())
        if err != nil {
            panic("Failed to open template '" + fi.Name() + "'")
        }
        content, err := ioutil.ReadAll(f)
        if err != nil {
            panic("Failed to read content from file '" + fi.Name() + "'")
        }
        f.Close()
        tmpl := template.Must(layout.Clone())
        _, err = tmpl.Parse(string(content))
        if err != nil {
            panic("Failed to parse contents of '" + fi.Name() + "' as template")
        }
        result[fi.Name()] = tmpl
    }
    return result
}
```

controller/g.go

```go
package controller

import "html/template"

var (
    homeController home
    templates      map[string]*template.Template
)

func init() {
    templates = PopulateTemplates()
}

// Startup func
func Startup() {
    homeController.registerRoutes()
}
```

controller/home.go

```go
package controller

import (
    "net/http"

    "github.com/bonfy/go-mega-code/vm"
)

type home struct{}

func (h home) registerRoutes() {
    http.HandleFunc("/", indexHandler)
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
    vop := vm.IndexViewModelOp{}
    v := vop.GetVM()
    templates["index.html"].Execute(w, &v)
}
```

这里将 匿名函数 实名成 indexHandler，并将所有的构造 indexviewmodel 的逻辑全部移到了 `vm/index.go` 中的 GetVM 方法里

最终我们的结构优化成了下图的树状结构，这样有利于我们以后的扩展。

```
go-mega-code
├── controller
│   ├── g.go
│   ├── home.go
│   └── utils.go
├── main.go
├── model
│   ├── post.go
│   └── user.go
├── templates
│   ├── _base.html
│   └── content
│       └── index.html
└── vm
    ├── g.go
    └── index.go
```

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/ed49bb8644f4aeb3da4d7b7c8ea504ed22d9a653)

## 用户登录表单

在将整个项目优化结构之后，我们建立登陆表单就非常简单了。

按照 index 的做法，login表单 我们其实需要，一个 `template`, 一个 `vm`, 以及一个 `handler`（其实后面基本上所有的加页面的做法也是类似）

templates/\_base.html

```markup
...
<div>
    Blog: 
    <a href="/">Home</a>
    <a href="/login">Login</a>
</div>
...
```

templates/content/login.html

```markup
{{define "content"}}
    <h1>Login</h1>
    <form action="/login" method="post" name="login">
        <p><input type="text" name="username" value="" placeholder="Username or Email"></p>
        <p><input type="password" name="password" value="" placeholder="Password"></p>
        <p><input type="submit" name="submit" value="Login"></p>
    </form>
{{end}}
```

login.html 还是继承 \_base.html 只要关注 content 的内容就行了

vm/login.go

```go
package vm

// LoginViewModel struct
type LoginViewModel struct {
    BaseViewModel
}

// LoginViewModelOp strutc
type LoginViewModelOp struct{}

// GetVM func
func (LoginViewModelOp) GetVM() LoginViewModel {
    v := LoginViewModel{}
    v.SetTitle("Login")
    return v
}
```

这里 `v.SetTitle` 就是用了 `匿名组合` 的特性，继承了 BaseViewModel 的 `SetTitle` 方法

controller/home.go 中加入 loginHandler

controller/home.go

```go
func (h home) registerRoutes() {
    ...
    http.HandleFunc("/login", loginHandler)
}

...

func loginHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "login.html"
    vop := vm.LoginViewModelOp{}
    v := vop.GetVM()
    templates[tpName].Execute(w, &v)
}

...
```

此时，你可以验证结果了, 运行该应用，在浏览器的地址栏中输入 `http://localhost:8888/`然后点击顶部导航栏中的`Login`链接来查看新的登录表单。

![](/files/-LNJQ7iTInNVLxk60Yey)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/3e42c84122344d4024968f518bc48cb56c38fac3)

## 接收表单数据

目前我们点击 `Login` 按钮，页面发现没有变化，其实我们后台还是接收到了这个请求，只是依然返回的是这个页面，接下来我们要对POST请求和GET请求分别做处理。

controller/home.go

```go
...

func loginHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "login.html"
    vop := vm.IndexViewModelOp{}
    v := vop.GetVM()
    if r.Method == http.MethodGet {
        templates[tpName].Execute(w, &v)
    }
    if r.Method == http.MethodPost {
        r.ParseForm()
        username := r.Form.Get("username")
        password := r.Form.Get("password")
        fmt.Fprintf(w, "Username:%s Password:%s", username, password)
    }
}
```

> Tip: html 中的form submit 是 Post 方法，简单说明下，不过相信大家都懂

修改 loginHandler 对 `MethodGet` 和 `MethodPost` 分别处理，`MethodPost` 接受 form post，运行后在login页面输入用户名、密码 点击Login，显示结果

![](/files/-LNJQ7iYxiaej_I8YFu_)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/f6e6b277400a03c9ceaa0231e2d3e5f794fa047f)

## 表单后端验证

表单验证分为服务器前端验证 与 后端验证，比如验证 输入字符个数、正则匹配等，一般来说 用户名密码正确性检查只能在后端验证，其它前后端验证都可以，不过为了减少服务器压力与加强用户体验，字符长度等检查 一般放在前端做检查。

由于本教程主要是 **Go** 后端 web教程，这里简单的做一个 后端检查 的示例

LoginModelView里加入 Errs 字段，用于输出检查的错误返回

vm/login.go

```go
...

type LoginViewModel struct {
    BaseViewModel
    Errs []string
}

// AddError func
func (v *LoginViewModel) AddError(errs ...string) {
    v.Errs = append(v.Errs, errs...)
}
...
```

login.html 中加入判断是否有错误，以及错误输出

templates/content/login.html

```markup
...
    {{if .Errs}}
    <ul>
        {{range .Errs}}
            <li>{{.}}</li>
         {{end}}
    </ul>
    {{end}}
...
```

controller/home.go

```go
...

func check(username, password string) bool {
    if username == "bonfy" && password == "abc123" {
        return true
    }
    return false
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "login.html"
    vop := vm.LoginViewModelOp{}
    v := vop.GetVM()

    if r.Method == http.MethodGet {
        templates[tpName].Execute(w, &v)
    }
    if r.Method == http.MethodPost {
        r.ParseForm()
        username := r.Form.Get("username")
        password := r.Form.Get("password")

        if len(username) < 3 {
            v.AddError("username must longer than 3")
        }

        if len(password) < 6 {
            v.AddError("password must longer than 6")
        }

        if !check(username, password) {
            v.AddError("username password not correct, please input again")
        }

        if len(v.Errs) > 0 {
            templates[tpName].Execute(w, &v)
        } else {
            http.Redirect(w, r, "/", http.StatusSeeOther)
        }
    }
}
```

再次运行，在login页面随便输入用户名密码，如果不符合规范，就会有后端验证提示你重新输入。

![](/files/-LNJQ7iacBbgiQCOd2WU)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/cf3fa723ee0cf67c6b1ccee6c8c7d4865a443acd)
>
> Notice: 显示 Error , Flask-Mega 教程里面用的是 flash (flash-messages)，我们这边是将错误信息直接 render 到了页面上，其实也是可以用 flash 的，不过 Go 的 Session 目前没有特别好的第三方插件，我们现在还没有用到第三方包，就用了原生的渲染。后续集成Session之后会有例子用到flash。先在此说明下，耐心往下看。

## Links

* [目录](/)
* 上一节: [03-Template-Advance](/03-template-advance)
* 下一节: [05-Database](/05-database)


# 05-Database

本章的主题是重中之重！大多数应用都需要持久化存储数据，并高效地执行的增删查改的操作，*数据库* 为此而生。

我们将第一次引入第三方库 [Gorm](https://github.com/jinzhu/gorm) 来帮助我们实现 ORM

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/05-Database), [Diff](https://github.com/bonfy/go-mega-code/compare/04-Web-Form...05-Database), [Zip](https://github.com/bonfy/go-mega-code/archive/v0.5.zip)

## 数据库及ORM选择

数据库被划分为两大类，遵循关系模型的一类是 `关系数据库`，另外的则是 `非关系数据库`，简称 `NoSQL`，表现在它们不支持流行的关系查询语言SQL。这里我们打算使用的是 `mysql`, 这是一种非常流行的关系型数据库，也应该是应用最广泛的数据库了。

ORM我们选用`Gorm`, 应该是因为它的Star数遥遥领先吧。

首先我们利用下面的命令安装 `Gorm`

```
go get -u github.com/jinzhu/gorm
```

## 数据库模型

定义数据库中一张表及其字段的类，通常叫做数据模型。ORM (Gorm) 会将类的实例关联到数据库表中的数据行，并翻译相关操作

![](https://blog.miguelgrinberg.com/static/images/mega-tutorial/ch04-users-posts.png)

id字段通常存在于所有模型并用作主键。每个用户都会被数据库分配一个id值，并存储到这个字段中。大多数情况下，主键都是数据库自动赋值的，我只需要提供id字段作为主键即可。

* user表: username，email和password\_hash字段被定义为字符串（数据库术语中的VARCHAR），并指定其最大长度，以便数据库可以优化空间使用率。 username和email字段的用途不言而喻，password\_hash字段值得提一下。 我想确保我正在构建的应用采用安全最佳实践，因此我不会将用户密码明文存储在数据库中。 明文存储密码的问题是，如果数据库被攻破，攻击者就会获得密码，这对用户隐私来说可能是毁灭性的。 如果使用哈希密码，这就大大提高了安全性。 这将是另一章的主题，所以现在不需分心。
* post表: 将具有必须的id、用户动态的body和timestamp字段。 除了这些预期的字段之外，我还添加了一个user\_id字段，将该用户动态链接到其作者。 你已经看到所有用户都有一个唯一的id主键， 将用户动态链接到其作者的方法是添加对用户id的引用，这正是user\_id字段所在的位置。 这个user\_id字段被称为外键。 上面的数据库图显示了外键作为该字段和它引用的表的id字段之间的链接。 这种关系被称为一对多，因为“一个”用户写了“多”条动态。

另外 我们后续还会有粉丝这种多对多的关系

![](https://blog.miguelgrinberg.com/static/images/mega-tutorial/ch08-followers-schema.png)

model/user.go

```go
package model

// User struct
type User struct {
    ID           int    `gorm:"primary_key"`
    Username     string `gorm:"type:varchar(64)"`
    Email        string `gorm:"type:varchar(120)"`
    PasswordHash string `gorm:"type:varchar(128)"`
    Posts        []Post
    Followers    []*User `gorm:"many2many:follower;association_jointable_foreignkey:follower_id"`
}
```

> Notice: 此处应该是`gorm:"type:varchar(64)"` 当时写的Github源码有错，不过只影响建表时的varchar size，请注意

model/post.go

```go
package model

import (
    "time"
)

// Post struct
type Post struct {
    ID        int `gorm:"primary_key"`
    UserID    int
    User      User
    Body      string     `gorm:"type:varchar(180)"`
    Timestamp *time.Time `sql:"DEFAULT:current_timestamp"`
}
```

model/g.go

```go
package model

import (
    "log"

    "github.com/bonfy/go-mega-code/config"
    "github.com/jinzhu/gorm"
)

var db *gorm.DB

// SetDB func
func SetDB(database *gorm.DB) {
    db = database
}

// ConnectToDB func
func ConnectToDB() *gorm.DB {
    connectingStr := config.GetMysqlConnectingString()
    log.Println("Connet to db...")
    db, err := gorm.Open("mysql", connectingStr)
    if err != nil {
        panic("Failed to connect database")
    }
    db.SingularTable(true)
    return db
}
```

其实如果作为演示项目，我们可以将`connectingStr := config.GetMysqlConnectingString()` 直接写入实际的演示数据库地址，不过为了追求和实际项目中的将配置文件单独放置的效果，我们还是将实际项目中的方法演示给大家。

多引入一个第三方package `github.com/spf13/viper`

```
go get -u github.com/spf13/viper
```

设置 config 文件， `config.yml`, 可以放在项目目录下 (实际 Source Code 中 git ignore 了实际的 config.yml, 存放了 config.yml.sample)

config.yml

```
mysql:
  charset: utf8
  db: go-mega
  host: localhost
  password: password
  user: root
```

config/g.go

```go
package config

import (
    "fmt"

    "github.com/spf13/viper"
)

func init() {
    projectName := "go-mega"
    getConfig(projectName)
}

func getConfig(projectName string) {
    viper.SetConfigName("config") // name of config file (without extension)

    viper.AddConfigPath(".")                                                // optionally look for config in the working directory
    viper.AddConfigPath(fmt.Sprintf("$HOME/.%s", projectName))              // call multiple times to add many search paths
    viper.AddConfigPath(fmt.Sprintf("/data/docker/config/%s", projectName)) // path to look for the config file in

    err := viper.ReadInConfig() // Find and read the config file
    if err != nil {             // Handle errors reading the config file
        panic(fmt.Errorf("Fatal error config file: %s", err))
    }
}

// GetMysqlConnectingString func
func GetMysqlConnectingString() string {
    usr := viper.GetString("mysql.user")
    pwd := viper.GetString("mysql.password")
    host := viper.GetString("mysql.host")
    db := viper.GetString("mysql.db")
    charset := viper.GetString("mysql.charset")

    return fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=%s&parseTime=true", usr, pwd, host, db, charset)
}
```

由于 **Go** 是编译型语言，我们不能像 Python 那样可以逐行的调试演示，我们就直接编写个程序，初始化表

cmd/db\_init/main.go

```go
package main

import (
    "log"

    "github.com/bonfy/go-mega-code/model"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

func main() {
    log.Println("DB Init ...")
    db := model.ConnectToDB()
    defer db.Close()
    model.SetDB(db)

    db.DropTableIfExists(model.User{}, model.Post{})
    db.CreateTable(model.User{}, model.Post{})
}
```

我们先在mysql中创建 `go-mega` 的数据库

```
$ mysql -u root -p
输入密码后
mysql> create DATABASE go-mega;
```

然后运行

```
go run cmd/db_init/main.go
```

![tables](/files/-LNJuuHNjsWc4VkDOfmK)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/2306009ddfbabd2f92406aa2cf233c2654e2fd7c)

## 初始化数据库数据

model/utils.go

```go
package model

import (
    "crypto/md5"
    "encoding/hex"
)

// GeneratePasswordHash : Use MD5
func GeneratePasswordHash(pwd string) string {
    hasher := md5.New()
    hasher.Write([]byte(pwd))
    pwdHash := hex.EncodeToString(hasher.Sum(nil))
    return pwdHash
}
```

model/user.go

```go
...
// SetPassword func: Set PasswordHash
func (u *User) SetPassword(password string) {
    u.PasswordHash = GeneratePasswordHash(password)
}

// CheckPassword func
func (u *User) CheckPassword(password string) bool {
    return GeneratePasswordHash(password) == u.PasswordHash
}

// GetUserByUsername func
func GetUserByUsername(username string) (*User, error) {
    var user User
    if err := db.Where("username=?", username).Find(&user).Error; err != nil {
        return nil, err
    }
    return &user, nil
}
```

model/post.go

```go
// GetPostsByUserID func
func GetPostsByUserID(id int) (*[]Post, error) {
    var posts []Post
    if err := db.Preload("User").Where("user_id=?", id).Find(&posts).Error; err != nil {
        return nil, err
    }
    return &posts, nil
}
```

> Notice: 特别注意这里的 Preload, 相当于预先的 Join Table，不然取得的 posts 就没有 User 信息

cmd/db\_init/main.go

```go
package main

import (
    "log"

    "github.com/bonfy/go-mega-code/model"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

func main() {
    log.Println("DB Init ...")
    db := model.ConnectToDB()
    defer db.Close()
    model.SetDB(db)

    db.DropTableIfExists(model.User{}, model.Post{})
    db.CreateTable(model.User{}, model.Post{})

    users := []model.User{
        {
            Username:     "bonfy",
            PasswordHash: model.GeneratePasswordHash("abc123"),
            Posts: []model.Post{
                {Body: "Beautiful day in Portland!"},
            },
        },
        {
            Username:     "rene",
            PasswordHash: model.GeneratePasswordHash("abc123"),
            Email:        "rene@test.com",
            Posts: []model.Post{
                {Body: "The Avengers movie was so cool!"},
                {Body: "Sun shine is beautiful"},
            },
        },
    }

    for _, u := range users {
        db.Debug().Create(&u)
    }
}
```

运行

```
$ go run cmd/db_init/main.go
```

user表 ![](/files/-LNJuuHRudb4neenmflf)

post表 ![](/files/-LNJuuHT5suhUoGrSFYF)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/834242739814757253916f6e2e6ef439dce96c0d)

## 完善view

现在我们的数据库里有了数据，我们就可以直接从数据库中取得数据，生成 index 的 viewmodel 了

修改 `vm/index.go` 的 GetVM func， 从数据库中获得数据

vm/index.go

```go
...
// GetVM func
func (IndexViewModelOp) GetVM() IndexViewModel {
    u1, _ := model.GetUserByUsername("rene")
    posts, _ := model.GetPostsByUserID(u1.ID)
    v := IndexViewModel{BaseViewModel{Title: "Homepage"}, *u1, *posts}
    return v
}
```

main.go

```go
package main

import (
    "net/http"

    "github.com/bonfy/go-mega-code/controller"
    "github.com/bonfy/go-mega-code/model"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

func main() {
    // Setup DB
    db := model.ConnectToDB()
    defer db.Close()
    model.SetDB(db)

    // Setup Controller
    controller.Startup()

    http.ListenAndServe(":8888", nil)
}
```

`main.go` 要初始化数据库连接，不然会报错。

另外 `import _ "github.com/jinzhu/gorm/dialects/mysql"` 确定你要使用的是 `mysql` 的 dialect

```
go run main.go
```

![](/files/-LNJuuHVAAR3p_-bZdU3)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/d966fd83cd177679322c20386f4788d500ba4026)
>
> Notice: 由于 Gorm 的 auto migration 并不像 SQLAlchemy 那么强大，我们在使用的时候要十分小心，这里我就直接没有使用（一般项目会预先定义好数据结构，而且后期修改也可以通过SQL操作），如果有兴趣的同学可以去看下 Gorm 的文档，还是有部分的 Migration 的能力的。

## Links

* [目录](/)
* 上一节: [04-Web-Form](/04-web-form)
* 下一节: [06-User-Login](/06-user-login)


# 06-User-Login

这章我将告诉你如何创建一个用户登录子系统。

你在[第四章](/04-web-form)中学会了如何创建用户登录表单，在[第五章](/05-database)中学会了运用数据库。本章将教你如何结合这两章的主题来创建一个简单的用户登录系统。

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/06-User-Login), [Diff](https://github.com/bonfy/go-mega-code/compare/05-Database...06-User-Login), [Zip](https://github.com/bonfy/go-mega-code/archive/v0.6.zip)

## Session

不知道 **Go** 有没有第三方库实现类似 Flask-Login 这样的登陆辅助(我也没有好好去找)，不过我们知道原理，基本上就是用 Session 实现的，对 Session 不了解的同学可以看下 [Session and Cookie](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/06.1.md)

这里我们又要引入一个第三方package来实现这个Session(如果不引入第三方库，自己处理还满麻烦的，所以就选择easy way)

```
$ go get -v github.com/gorilla/sessions
```

### 全局变量设置

在 controller/g.go 中设置将要用到的全局变量 `sessionName`, `store`,

> Tip: 设置全局变量其实是方便以后的更新，如果以后sessionName要改成 `flask-mega`,只要跑到全局设置的地方修改，不用到每个函数中修改，而且还容易改错。

store 初始化的时候可以设置 secret-key，这里直接 hard code 了，其实安全点的做法可以设置在配置文件里,这里就这样偷懒了吧

controller/g.go

```go
package controller

import (
    "html/template"

    "github.com/gorilla/sessions"
)

var (
    homeController home
    templates      map[string]*template.Template
    sessionName    string
    store          *sessions.CookieStore
)

func init() {
    templates = PopulateTemplates()
    store = sessions.NewCookieStore([]byte("something-very-secret"))
    sessionName = "go-mega"
}

// Startup func
func Startup() {
    homeController.registerRoutes()
}
```

### 将 session 操作封装

操作函数基本上就是 `GetSession`, `SetSession`, `ClearSession` 所有语言说道Session基本上就实现这三个基本的，后面属于自由发挥

controller/utils.go

```go
...

// session

func getSessionUser(r *http.Request) (string, error) {
    var username string
    session, err := store.Get(r, sessionName)
    if err != nil {
        return "", err
    }

    val := session.Values["user"]
    fmt.Println("val:", val)
    username, ok := val.(string)
    if !ok {
        return "", errors.New("can not get session user")
    }
    fmt.Println("username:", username)
    return username, nil
}

func setSessionUser(w http.ResponseWriter, r *http.Request, username string) error {
    session, err := store.Get(r, sessionName)
    if err != nil {
        return err
    }
    session.Values["user"] = username
    err = session.Save(r, w)
    if err != nil {
        return err
    }
    return nil
}

func clearSession(w http.ResponseWriter, r *http.Request) error {
    session, err := store.Get(r, sessionName)
    if err != nil {
        return err
    }

    session.Options.MaxAge = -1

    err = session.Save(r, w)
    if err != nil {
        return err
    }

    return nil
}
```

> Tip: 这里 clearSession 的操作是通过设置 MaxAge 为 负数来完成的。

### ListenAndServe 修改

main.go

```go
    ...
    controller.Startup()

    http.ListenAndServe(":8888", context.ClearHandler(http.DefaultServeMux))
```

这样我们就支持了 session 我们在 controller home中来使用

vm/login.go

```go
...

// CheckLogin func
func CheckLogin(username, password string) bool {
    user, err := model.GetUserByUsername(username)
    if err != nil {
        log.Println("Can not find username: ", username)
        log.Println("Error:", err)
        return false
    }
    return user.CheckPassword(password)
}
```

controller/home.go

```go
...

func (h home) registerRoutes() {
    http.HandleFunc("/", indexHandler)
    http.HandleFunc("/login", loginHandler)
    http.HandleFunc("/logout", logoutHandler)
}

...

func loginHandler(w http.ResponseWriter, r *http.Request) {
    ...

        if !vm.CheckLogin(username, password) {
            v.AddError("username password not correct, please input again")
        }

        if len(v.Errs) > 0 {
            templates[tpName].Execute(w, &v)
        } else {
            setSessionUser(w, r, username)
            http.Redirect(w, r, "/", http.StatusSeeOther)
        }
    }
}

func logoutHandler(w http.ResponseWriter, r *http.Request) {
    clearSession(w, r)
    http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
}
```

上面的代码完成了在 login 成功后设置 session，也加入了 logoutHanler

现在我们运行程序后，点击login 之后会发现，session `go-mega`

![](/files/-LNZWk6oAD0K7SyXSygV)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/6f54950b8a6faa86a648f05978e3eab6877343ee)

## middleware实现登陆控制

一般正常的需要登陆的网站，如果你要访问主页面，如果你没有登陆过，跳转到登陆界面，如果之前登陆过，则直接跳转到你要访问的页面。

现在我们访问根目录`http://127.0.0.1/`是不受登陆控制的，如果我们要给它加上必须登陆后才能访问，要怎么处理呢？

答案就是加上 middleware 中间层去判断是否存在session，类似于 Python 中的装饰器的作用

controller/middle.go

```go
package controller

import (
    "log"
    "net/http"
)

func middleAuth(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        username, err := getSessionUser(r)
        log.Println("middle:", username)
        if err != nil {
            log.Println("middle get session err and redirect to login")
            http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
        } else {
            next.ServeHTTP(w, r)
        }
    }
}
```

在路由上加入session 控制

```go
...
func (h home) registerRoutes() {
    http.HandleFunc("/", middleAuth(indexHandler))
    http.HandleFunc("/login", loginHandler)
    http.HandleFunc("/logout", middleAuth(logoutHandler))
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "index.html"
    vop := vm.IndexViewModelOp{}
    username, _ := getSessionUser(r)
    v := vop.GetVM(username)
    templates[tpName].Execute(w, &v)
}
...
```

现在我们访问到 indexHandler 的时候 middleAuth 保证是有session的，所以取出session，根据取出的user来获取viewmodel响应的，我们的 vm 也要调整

主要是将 User区分下，比如 `CurrentUser` 以后就代表登陆的用户，也就是Session的User，例如`ProfileUser` 则可以是你查看的任何人

vm/g.go

```go
package vm

// BaseViewModel struct
type BaseViewModel struct {
    Title       string
    CurrentUser string
}

// SetTitle func
func (v *BaseViewModel) SetTitle(title string) {
    v.Title = title
}

// SetCurrentUser func
func (v *BaseViewModel) SetCurrentUser(username string) {
    v.CurrentUser = username
}
```

vm/index.go

```go
...
// GetVM func
func (IndexViewModelOp) GetVM(username string) IndexViewModel {
    u1, _ := model.GetUserByUsername(username)
    posts, _ := model.GetPostsByUserID(u1.ID)
    v := IndexViewModel{BaseViewModel{Title: "Homepage"}, *posts}
    v.SetCurrentUser(username)
    return v
}
```

templates/\_base.html

```markup
    ...
        <div>
            Blog: 
            <a href="/">Home</a>
            {{if .CurrentUser}}
            <a href="/logout">Logout</a>
            {{else}}
            <a href="/login">Login</a>
            {{end}}
        </div>
    ...
```

templates/content/index.html

```markup
{{define "content"}}
    <h1>Hello, {{.CurrentUser}}!</h1>

    {{range .Posts}}
        <div><p>{{ .User.Username }} says: <b>{{ .Body }}</b></p></div>
    {{end}}
{{end}}
```

然后运行，输入 `http://127.0.0.1/` 会直接跳转到登陆页面；在正确输入用户名、密码正确登陆后，右上角变成了 `Logout`

![06-02](/files/-LNZWk6tfk5vjRq_RZmY)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/4ce33f3dbdd4ebb828edcb553caa4e32e9bf0318)

## 加入register

templates/content/register.html

```markup
{{define "content"}}
    <h1>Register</h1>
    <form action="/register" method="post" name="register">
        <p><input type="text" name="username" value="" placeholder="Username"></p>
        <p><input type="text" name="email" value="" placeholder="Email"></p>
        <p><input type="password" name="pwd1" value="" placeholder="Password"></p>
        <p><input type="password" name="pwd2" value="" placeholder="Password"></p>
        <p><input type="submit" name="submit" value="Register"></p>
    </form>

    <p>Have account? <a href="/login">Click to Login!</a></p>

    {{if .Errs}}
    <ul>
        {{range .Errs}}
            <li>{{.}}</li>
         {{end}}
    </ul>
    {{end}}
{{end}}
```

在 login 的页面上加入 `register` 链接

templates/content/login.html

```markup
...
 <p>New User? <a href="/register">Click to Register!</a></p>
...
```

addUser 的调用是 vm调用model的，controller 调用 vm的，所以各层都要建立 AddUser函数

model/user.go

```go
...
// AddUser func
func AddUser(username, password, email string) error {
    user := User{Username: username, Email: email}
    user.SetPassword(password)
    return db.Create(&user).Error
}
```

vm/register.go

```go
package vm

import (
    "log"

    "github.com/bonfy/go-mega-code/model"
)

// RegisterViewModel struct
type RegisterViewModel struct {
    LoginViewModel
}

// RegisterViewModelOp struct
type RegisterViewModelOp struct{}

// GetVM func
func (RegisterViewModelOp) GetVM() RegisterViewModel {
    v := RegisterViewModel{}
    v.SetTitle("Register")
    return v
}

// CheckUserExist func
func CheckUserExist(username string) bool {
    _, err := model.GetUserByUsername(username)
    if err != nil {
        log.Println("Can not find username: ", username)
        return true
    }
    return false
}

// AddUser func
func AddUser(username, password, email string) error {
    return model.AddUser(username, password, email)
}
```

controller/utils.go

```go
...
// Login Check
func checkLen(fieldName, fieldValue string, minLen, maxLen int) string {
    lenField := len(fieldValue)
    if lenField < minLen {
        return fmt.Sprintf("%s field is too short, less than %d", fieldName, minLen)
    }
    if lenField > maxLen {
        return fmt.Sprintf("%s field is too long, more than %d", fieldName, maxLen)
    }
    return ""
}

func checkUsername(username string) string {
    return checkLen("Username", username, 3, 20)
}

func checkPassword(password string) string {
    return checkLen("Password", password, 6, 50)
}

func checkEmail(email string) string {
    if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, email); !m {
        return fmt.Sprintf("Email field not a valid email")
    }
    return ""
}

func checkUserPassword(username, password string) string {
    if !vm.CheckLogin(username, password) {
        return fmt.Sprintf("Username and password is not correct.")
    }
    return ""
}

func checkUserExist(username string) string {
    if !vm.CheckUserExist(username) {
        return fmt.Sprintf("Username already exist, please choose another username")
    }
    return ""
}

// checkLogin()
func checkLogin(username, password string) []string {
    var errs []string
    if errCheck := checkUsername(username); len(errCheck) > 0 {
        errs = append(errs, errCheck)
    }
    if errCheck := checkPassword(password); len(errCheck) > 0 {
        errs = append(errs, errCheck)
    }
    if errCheck := checkUserPassword(username, password); len(errCheck) > 0 {
        errs = append(errs, errCheck)
    }
    return errs
}

// checkRegister()
func checkRegister(username, email, pwd1, pwd2 string) []string {
    var errs []string
    if pwd1 != pwd2 {
        errs = append(errs, "2 password does not match")
    }
    if errCheck := checkUsername(username); len(errCheck) > 0 {
        errs = append(errs, errCheck)
    }
    if errCheck := checkPassword(pwd1); len(errCheck) > 0 {
        errs = append(errs, errCheck)
    }
    if errCheck := checkEmail(email); len(errCheck) > 0 {
        errs = append(errs, errCheck)
    }
    if errCheck := checkUserExist(username); len(errCheck) > 0 {
        errs = append(errs, errCheck)
    }
    return errs
}

// addUser()
func addUser(username, password, email string) error {
    return vm.AddUser(username, password, email)
}
```

我们顺便将各种后端验证（check function） 都移到了 controller/utils.go，这样便于以后的扩展,而且 controller/home.go 也相应简化了

controller/home.go

```go
package controller

import (
    "log"
    "net/http"

    "github.com/bonfy/go-mega-code/vm"
)

type home struct{}

func (h home) registerRoutes() {
    http.HandleFunc("/logout", middleAuth(logoutHandler))
    http.HandleFunc("/login", loginHandler)
    http.HandleFunc("/register", registerHandler)
    http.HandleFunc("/", middleAuth(indexHandler))
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "index.html"
    vop := vm.IndexViewModelOp{}
    username, _ := getSessionUser(r)
    v := vop.GetVM(username)
    templates[tpName].Execute(w, &v)
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "login.html"
    vop := vm.LoginViewModelOp{}
    v := vop.GetVM()

    if r.Method == http.MethodGet {
        templates[tpName].Execute(w, &v)
    }
    if r.Method == http.MethodPost {
        r.ParseForm()
        username := r.Form.Get("username")
        password := r.Form.Get("password")

        errs := checkLogin(username, password)
        v.AddError(errs...)

        if len(v.Errs) > 0 {
            templates[tpName].Execute(w, &v)
        } else {
            setSessionUser(w, r, username)
            http.Redirect(w, r, "/", http.StatusSeeOther)
        }
    }
}

func registerHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "register.html"
    vop := vm.RegisterViewModelOp{}
    v := vop.GetVM()

    if r.Method == http.MethodGet {
        templates[tpName].Execute(w, &v)
    }
    if r.Method == http.MethodPost {
        r.ParseForm()
        username := r.Form.Get("username")
        email := r.Form.Get("email")
        pwd1 := r.Form.Get("pwd1")
        pwd2 := r.Form.Get("pwd2")

        errs := checkRegister(username, email, pwd1, pwd2)
        v.AddError(errs...)

        if len(v.Errs) > 0 {
            templates[tpName].Execute(w, &v)
        } else {
            if err := addUser(username, pwd1, email); err != nil {
                log.Println("add User error:", err)
                w.Write([]byte("Error insert database"))
                return
            }
            setSessionUser(w, r, username)
            http.Redirect(w, r, "/", http.StatusSeeOther)
        }
    }
}

func logoutHandler(w http.ResponseWriter, r *http.Request) {
    clearSession(w, r)
    http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
}
```

运行程序，我们就有了注册页面了

![06-03](/files/-LNZWk72eRDet_S4A4wQ)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/2940d8a8ada7e9ea2d214495f7dc6958c4e45c16)

## Links

* [目录](/)
* 上一节: [05-Database](/05-database)
* 下一节: [07-Profile-Page-And-Avatar](/07-profile-page-and-avatar)


# 07-Profile-Page-And-Avatar

本章将致力于为应用添加个人主页。个人主页用来展示用户的相关信息，其个人信息由本人录入。 我将为你展示如何动态地生成每个用户的主页，并提供一个编辑页面给他们来更新个人信息。

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/07-Profile-Page), [Diff](https://github.com/bonfy/go-mega-code/compare/06-User-Login...07-Profile-Page), [Zip](https://github.com/bonfy/go-mega-code/archive/v0.7.zip)

## Profile Page

作为创建个人主页的第一步，让我们为其URL /user/ 新建一个对应的视图函数。

我们还是老套路 一个 `vm` 加一个 `page` 另外再加一个 `controller` （`model` 暂时不涉及新的model）

vm/profile.go

```go
package vm

import "github.com/bonfy/go-mega-code/model"

// ProfileViewModel struct
type ProfileViewModel struct {
    BaseViewModel
    Posts       []model.Post
    ProfileUser model.User
}

// ProfileViewModelOp struct
type ProfileViewModelOp struct{}

// GetVM func
func (ProfileViewModelOp) GetVM(sUser, pUser string) (ProfileViewModel, error) {
    v := ProfileViewModel{}
    v.SetTitle("Profile")
    u1, err := model.GetUserByUsername(pUser)
    if err != nil {
        return v, err
    }
    posts, _ := model.GetPostsByUserID(u1.ID)
    v.ProfileUser = *u1
    v.Posts = *posts
    v.SetCurrentUser(sUser)
    return v, nil
}
```

`_base.html` 在登陆情况下 加入 Profile 的链接

templates/\_base.html

```markup
...
    <div>
        Blog: 
        <a href="/">Home</a>
        {{if .CurrentUser}}
        <a href="/user/{{.CurrentUser}}">Profile</a>
        <a href="/logout">Logout</a>
        {{else}}
        <a href="/login">Login</a>
        {{end}}
    </div>
...
```

templates/content/profile.html

```markup
{{define "content"}}
    <h1>User: {{.ProfileUser.Username}}</h1>

    <hr/>

    {{range .Posts}}
    <p>
    {{ .User.Username }} says: <b>{{ .Body }}</b>
    </p>
    {{end}}
{{end}}
```

加入 profileController

controller/home.go

```go
package controller

import (
    "fmt"
    "log"
    "net/http"

    "github.com/bonfy/go-mega-code/vm"
    "github.com/gorilla/mux"
)

type home struct{}

func (h home) registerRoutes() {
    r := mux.NewRouter()
    r.HandleFunc("/logout", middleAuth(logoutHandler))
    r.HandleFunc("/login", loginHandler)
    r.HandleFunc("/register", registerHandler)
    r.HandleFunc("/user/{username}", middleAuth(profileHandler))
    r.HandleFunc("/", middleAuth(indexHandler))

    http.Handle("/", r)
}

...


func profileHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "profile.html"
    vars := mux.Vars(r)
    pUser := vars["username"]
    sUser, _ := getSessionUser(r)
    vop := vm.ProfileViewModelOp{}
    v, err := vop.GetVM(sUser, pUser)
    if err != nil {
        msg := fmt.Sprintf("user ( %s ) does not exist", pUser)
        w.Write([]byte(msg))
        return
    }
    templates[tpName].Execute(w, &v)
}

...
```

这里面由于要实现 flask 那样的 `/user/username` 的效果，快速的方法是引入 `gorilla/mux` 的第三方package

```
$ go get -v github.com/gorilla/mux
```

然后注意 `registerRoutes` 函数，里面原来的 `http.handleFunc` 全部替换成 `r.handleFunc` 交给第三方的 `gorilla/mux`来处理，这样我们就可以在 handler里面使用 `mux.Vars` 来解析URL里面的`{username}`了

另外 严谨起见，这里在 profileHandler 前面加了 auth check,其实不加的话也是OK的，区别就是在登陆之前能不能查看特定 user 的 Profile

我们运行程序，登陆后点击 `Profile`的链接，就能查看到结果了。

当然你在地址栏里面直接输入 `http://127.0.0.1/user/username`如果存在，就显示 User 的 Profile,不存在就提示 `user (username) does not exist`

![07-01](/files/-LNomP3Okp5MRDsz6Ddq)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/db97f9013662e1a3de2c029937f13b52dc9ac202)

## Avatar

我相信你也觉得我刚刚建立的个人主页非常枯燥乏味。为了使它们更加有趣，我将添加用户头像。与其在服务器上处理大量的上传图片，我将使用[Gravatar](http://gravatar.com/)为所有用户提供图片服务。

Gravatar服务使用起来非常简单。 要请求给定用户的图片，使用格式为`https://www.gravatar.com/avatar/` 的URL即可，其中 `hash` 是用户的电子邮件地址的MD5哈希值。 在下面，你可以看到如何生成电子邮件为<john@example.com>的用户的Gravatar URL：

如果你想看一个实际的例子，我自己的Gravatar URL是`https://www.gravatar.com/avatar/c60f4fa4bf54012b80bc140aab0fc2bc`。Gravatar返回的图片如下：

![avatar](/files/-LNonYHJbupOSnwGLwsD)

默认情况下 是 80\*80 , 但可以通过向URL的查询字符串添加s参数来请求不同大小的图片。如: `https://www.gravatar.com/avatar/c60f4fa4bf54012b80bc140aab0fc2bc?s=128`

另一个可传递给Gravatar的有趣参数是d，它让Gravatar为没有向服务注册头像的用户提供的随机头像。 我最喜欢的随机头像类型是“identicon”，它为每个邮箱都返回一个漂亮且不重复的几何设计图片。 如下：

![](https://camo.githubusercontent.com/001fbd04e5b6ac6ef40f68255de3d825872e3143/687474703a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f343936313532382d643039363239633532383639633630352e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430)

请注意，一些Web浏览器插件（如Ghostery）会屏蔽Gravatar图像，因为它们认为Automattic（Gravatar服务的所有者）可以根据你发送的获取头像的请求来判断你正在访问的网站。 如果在浏览器中看不到头像，你在排查问题的时候可以考虑以下是否在浏览器中安装了此类插件。

由于头像与用户相关联，所以将生成头像URL的逻辑添加到用户模型是有道理的。

如果你对Gravatar服务很有兴趣，可以学习他们的[文档](https://gravatar.com/site/implement/images)。

理论说好了，我们来写代码，由于 **Go** 原生的Template 不像 Jinja2 那么好支持函数（ **Go** Template 是支持自定义函数的，只是要在 template.New().Funcs() 中预先传入，与我们已有的 `PopulateTemplates`函数集成上有点难度）

> Notice: Go Template 支持类的 Func，不用预先传入，这里 Avatar 字段不是特别的必要了，特此说明，可以参见 [12-Dates-And-Times](/12-dates-and-times)的用法

所以这里发挥主观能动性，直接将 Avatar 作为字段放入数据库中，等于冗余了Avatar数据，但是减少了我们coding的难度（我们也乘此机会，fix下上次提到的Gorm format问题）

虽然我们 `GeneratePasswordHash` 就是 MD5 方法，不过为了逻辑的清晰，我们可以像下面代码这样处理。

model/utils.go

```go
package model

import (
    "crypto/md5"
    "encoding/hex"
)

// GeneratePasswordHash : Use MD5
func GeneratePasswordHash(pwd string) string {
    return Md5(pwd)
}

// Md5 func
func Md5(origin string) string {
    hasher := md5.New()
    hasher.Write([]byte(origin))
    return hex.EncodeToString(hasher.Sum(nil))
}
```

model/user.go

```go
...
// 说明: User 加入 Avatar、AboutMe、LastSeen 字段
type User struct {
    ID           int    `gorm:"primary_key"`
    Username     string `gorm:"type:varchar(64)"`
    Email        string `gorm:"type:varchar(120)"`
    PasswordHash string `gorm:"type:varchar(128)"`
    LastSeen     *time.Time
    AboutMe      string `gorm:"type:varchar(140)"`
    Avatar       string `gorm:"type:varchar(200)"`
    Posts        []Post
    Followers    []*User `gorm:"many2many:follower;association_jointable_foreignkey:follower_id"`
}
...

// 说明: 在增加User 的时候，直接设置Avatar

// SetAvatar func
func (u *User) SetAvatar(email string) {
    u.Avatar = fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=identicon", Md5(email))
}

// AddUser func
func AddUser(username, password, email string) error {
    user := User{Username: username, Email: email}
    user.SetPassword(password)
    user.SetAvatar(email)
    return db.Create(&user).Error
}
...
```

cmd/db\_init/main.go

```go
package main

import (
    "fmt"
    "log"

    "github.com/bonfy/go-mega-code/model"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

func main() {
    log.Println("DB Init ...")
    db := model.ConnectToDB()
    defer db.Close()
    model.SetDB(db)

    db.DropTableIfExists(model.User{}, model.Post{})
    db.CreateTable(model.User{}, model.Post{})

    users := []model.User{
        {
            Username:     "bonfy",
            PasswordHash: model.GeneratePasswordHash("abc123"),
            Email:        "i@bonfy.im",
            Avatar:       fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=identicon", model.Md5("i@bonfy.im")),
            Posts: []model.Post{
                {Body: "Beautiful day in Portland!"},
            },
        },
        {
            Username:     "rene",
            PasswordHash: model.GeneratePasswordHash("abc123"),
            Email:        "rene@test.com",
            Avatar:       fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=identicon", model.Md5("rene@test.com")),
            Posts: []model.Post{
                {Body: "The Avengers movie was so cool!"},
                {Body: "Sun shine is beautiful"},
            },
        },
    }

    for _, u := range users {
        db.Debug().Create(&u)
    }
}
```

这样再运行

```
$ go run cmd/db_init/main.go
```

数据库中的数据就有了 Avatar 这个字段

![07-02](/files/-LNomP3T81_7p4_am1dG)

现在我们再在 `profile.html`加入Avatar

这样我的个人主页的顶部有一个不错的大头像，不止如此，底下的所有用户动态都会有一个小头像。

templates/content/profile.html

```markup
{{define "content"}}

    <table>
        <tr valign="top">
            <td><img src="{{.ProfileUser.Avatar}}&s=128"></td>
            <td><h1>User: {{.ProfileUser.Username}}</h1></td>
        </tr>
    </table>

    <hr/>

    {{range .Posts}}
        <table>
            <tr valign="top">
                <td><img src="{{.User.Avatar}}&s=36"></td>
                <td>{{ .User.Username }} says:<br>{{ .Body }}</td>
            </tr>
        </table>
    {{end}}

{{end}}
```

运行程序

```
$ go run main.go
```

![07-03](/files/-LNomP3VeVE89LMAU_On)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/b9549261e9d4f0e55ea9612879101d36f32ee388)

## More Info

新增的个人主页存在的一个问题是，真正显示的内容不够丰富。 用户喜欢在个人主页上展示他们的相关信息，所以我会让他们写一些自我介绍并在这里展示。 我也将跟踪每个用户最后一次访问该网站的时间，并显示在他们的个人主页上。

新字段 `last_seen` 和 `about_me` 已经在前面一起创建过了，我们只要在页面中加入就行了。

现在 `model/user.go` 中加入 `UpdateLastSeen` 函数

model/user.go

```go
...

// UpdateUserByUsername func
func UpdateUserByUsername(username string, contents map[string]interface{}) error {
    item, err := GetUserByUsername(username)
    if err != nil {
        return err
    }
    return db.Model(item).Updates(contents).Error
}

// UpdateLastSeen func
func UpdateLastSeen(username string) error {
    contents := map[string]interface{}{"last_seen": time.Now()}
    return UpdateUserByUsername(username, contents)
}
```

在 middleAuth 中，如果判断用户登陆了，就更新他的 `LastSeen` 时间 (middleAuth的用法类似于Python 里面的装饰器用法)

> Tip: 我们一般是不会在 controller 层直接和 model 层打交道，一般会通过 vm 层去处理，但是由于 middle 层不具有具体的view，我们这里破例直接调用 model 中的方法。 当然考究点，你可以建立一个 middle 的vm，在里面新建一个 UpdateLastSeen 再在 controller/middle 中调用，也是可以的

controller/middle.go

```go
package controller

import (
    "log"
    "net/http"

    "github.com/bonfy/go-mega-code/model"
)

func middleAuth(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        username, err := getSessionUser(r)
        log.Println("middle:", username)
        if username != "" {
            log.Println("Last seen:", username)
            model.UpdateLastSeen(username)
        }
        if err != nil {
            log.Println("middle get session err and redirect to login")
            http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
        } else {
            next.ServeHTTP(w, r)
        }
    }
}

...
```

在 `profile.html` 中加入 `AboutMe` 以及 `LastSeen`

templates/content/profile.html

```markup
{{define "content"}}

    <table>
        <tr valign="top">
            <td><img src="{{.ProfileUser.Avatar}}&s=128"></td>
            <td>
                <h1>User: {{.ProfileUser.Username}}</h1>
                {{if .ProfileUser.AboutMe}}
                    <p>{{ .ProfileUser.AboutMe }}</p>
                {{end}}

                {{if .ProfileUser.LastSeen}}
                    <p>Last seen on: {{ .ProfileUser.LastSeen }}</p>
                {{end}}
            </td>
        </tr>
    </table>

    <hr/>

    {{range .Posts}}
        <table>
            <tr valign="top">
                <td><img src="{{.User.Avatar}}&s=36"></td>
                <td>{{ .User.Username }} says:<br>{{ .Body }}</td>
            </tr>
        </table>
    {{end}}

{{end}}
```

![07-04](/files/-LNomP3YBiYyXZ8pl3ua)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/4e8cba8d0084e6020ded1534ab93026cf919ef14)

## Edit Profile

我还需要给用户一个表单，让他们输入一些个人资料。 表单将允许用户更改他们的用户名，并且写一些个人介绍，以存储在新的about\_me字段中。

其实细分的话分两步：

* 在`Profile`页面加入 Edit 的链接
* 增加一个 `profile_edit` 的页面

加入 Edit 链接: vm 增加 Editable 字段， 然后在 `profile.html` 中加入链接

model/profile.go

```go
package vm

import "github.com/bonfy/go-mega-code/model"

// ProfileViewModel struct
type ProfileViewModel struct {
    BaseViewModel
    Posts       []model.Post
    Editable    bool
    ProfileUser model.User
}

// ProfileViewModelOp struct
type ProfileViewModelOp struct{}

// GetVM func
func (ProfileViewModelOp) GetVM(sUser, pUser string) (ProfileViewModel, error) {
    v := ProfileViewModel{}
    v.SetTitle("Profile")
    u1, err := model.GetUserByUsername(pUser)
    if err != nil {
        return v, err
    }
    posts, _ := model.GetPostsByUserID(u1.ID)
    v.ProfileUser = *u1
    v.Editable = (sUser == pUser)
    v.Posts = *posts
    v.SetCurrentUser(sUser)
    return v, nil
}
```

templates/content/profile.html

```markup
{{define "content"}}

    <table>
        <tr valign="top">
            <td><img src="{{.ProfileUser.Avatar}}&s=128"></td>
            <td>
                <h1>User: {{.ProfileUser.Username}}</h1>
                {{if .ProfileUser.AboutMe}}
                    <p><pre>{{ .ProfileUser.AboutMe }}</pre></p>
                {{end}}

                {{if .ProfileUser.LastSeen}}
                    <p>Last seen on: {{ .ProfileUser.LastSeen }}</p>
                {{end}}

                {{if .Editable}}
                    <p><a href="/profile_edit">Edit your profile</a></p>
                {{end}}
            </td>
        </tr>
    </table>

    <hr/>
...
```

增加 `profile_edit` ，这个老套路

model/user.go

```go
...

// UpdateAboutMe func
func UpdateAboutMe(username, text string) error {
    contents := map[string]interface{}{"about_me": text}
    return UpdateUserByUsername(username, contents)
}
```

vm/profile\_edit.go

```go
package vm

import "github.com/bonfy/go-mega-code/model"

// ProfileEditViewModel struct
type ProfileEditViewModel struct {
    LoginViewModel
    ProfileUser model.User
}

// ProfileEditViewModelOp struct
type ProfileEditViewModelOp struct{}

// GetVM func
func (ProfileEditViewModelOp) GetVM(username string) ProfileEditViewModel {

    v := ProfileEditViewModel{}
    u, _ := model.GetUserByUsername(username)
    v.SetTitle("Profile Edit")
    v.SetCurrentUser(username)
    v.ProfileUser = *u
    return v
}

// UpdateAboutMe func
func UpdateAboutMe(username, text string) error {
    return model.UpdateAboutMe(username, text)
}
```

templates/content/profile\_edit.html

```markup
{{define "content"}}
    <h1>Profile Edit</h1>
    <p>Username: {{.ProfileUser.Username}}</p>
    <form action="/profile_edit" method="post" name="profile_edit">
        <p>About Me</p>
        <p><textarea  name="aboutme" rows="5" cols="80" value="" placeholder="about me">{{.ProfileUser.AboutMe}}</textarea></p>
        <p><input type="submit" name="submit" value="Save"></p>
    </form>

    {{if .Errs}}
    <ul>
        {{range .Errs}}
            <li>{{.}}</li>
         {{end}}
    </ul>
    {{end}}
{{end}}
```

最后在 controller 里面加入 `profileEditHandler`

controller/home.go

```go
...

func (h home) registerRoutes() {
    r := mux.NewRouter()
    r.HandleFunc("/logout", middleAuth(logoutHandler))
    r.HandleFunc("/login", loginHandler)
    r.HandleFunc("/register", registerHandler)
    r.HandleFunc("/user/{username}", middleAuth(profileHandler))
    r.HandleFunc("/profile_edit", middleAuth(profileEditHandler))
    r.HandleFunc("/", middleAuth(indexHandler))

    http.Handle("/", r)
}

...

func profileEditHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "profile_edit.html"
    username, _ := getSessionUser(r)
    vop := vm.ProfileEditViewModelOp{}
    v := vop.GetVM(username)
    if r.Method == http.MethodGet {
        err := templates[tpName].Execute(w, &v)
        if err != nil {
            log.Println(err)
        }
    }

    if r.Method == http.MethodPost {
        r.ParseForm()
        aboutme := r.Form.Get("aboutme")
        log.Println(aboutme)
        if err := vm.UpdateAboutMe(username, aboutme); err != nil {
            log.Println("update Aboutme error:", err)
            w.Write([]byte("Error update aboutme"))
            return
        }
        http.Redirect(w, r, fmt.Sprintf("/user/%s", username), http.StatusSeeOther)
    }
}
```

![07-05](/files/-LNomP3ewKvBVGm_3p2s)

![07-06](/files/-LNomP3g6o_jmiaNFnwW)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/a0a50dfc38c61949f065d0e02ef5bb0e89ffcdd0)

## Links

* [目录](/)
* 上一节: [06-User-Login](/06-user-login)
* 下一节: [08-Follower](/08-follower)


# 08-Follower

这章将告诉你如何实现类似于Twitter和其他社交网络的“粉丝”功能。

在本章中，我将更多地使用应用的数据库。 我希望应用的用户能够轻松便捷地关注其他用户。

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/08-Follower), [Diff](https://github.com/bonfy/go-mega-code/compare/07-Profile-Page...08-Follower), [Zip](https://github.com/bonfy/go-mega-code/archive/v0.8.zip)

## 完善用户模型

首先我们列下要实现的用户操作:

* 关注 - Follow
* 取消关注 - UnFollow
* 关注自己 - FollowSelf
* follower数量 - FollowersCount
* following数量 - FollowingCount
* 关注的Posts - FollowingPosts 这个将被用在页面美化那章中的登陆后的IndexView显示
* 判断是否被关注 - IsFollowedByUser
* (\*)创建Post - CreatePost  这个是我们为了方便创建数据加入的，和粉丝操作关系不大

> Tip: 这里 Followers 可以通过 Gorm 的 `Association("Followers")`来实现， Following 好像不支持(不确定，看了几遍文档没有找到)，不过这里我们一顿骚操作，自己来实现一下嘛

model/user.go

```go
...
// Follow func
// follow someone usr_id other.id follow_id u.id
func (u *User) Follow(username string) error {
    other, err := GetUserByUsername(username)
    if err != nil {
        return err
    }
    return db.Model(other).Association("Followers").Append(u).Error
}

// Unfollow func
func (u *User) Unfollow(username string) error {
    other, err := GetUserByUsername(username)
    if err != nil {
        return err
    }
    return db.Model(other).Association("Followers").Delete(u).Error
}

// FollowSelf func
func (u *User) FollowSelf() error {
    return db.Model(u).Association("Followers").Append(u).Error
}

// FollowersCount func
func (u *User) FollowersCount() int {
    return db.Model(u).Association("Followers").Count()
}

// FollowingIDs func
func (u *User) FollowingIDs() []int {
    var ids []int
    rows, err := db.Table("follower").Where("follower_id = ?", u.ID).Select("user_id, follower_id").Rows()
    if err != nil {
        log.Println("Counting Following error:", err)
        return ids
    }
    defer rows.Close()
    for rows.Next() {
        var id, followerID int
        rows.Scan(&id, &followerID)
        ids = append(ids, id)
    }
    return ids
}

// FollowingCount func
func (u *User) FollowingCount() int {
    ids := u.FollowingIDs()
    return len(ids)
}

// FollowingPosts func
func (u *User) FollowingPosts() (*[]Post, error) {
    var posts []Post
    ids := u.FollowingIDs()
    if err := db.Preload("User").Order("timestamp desc").Where("user_id in (?)", ids).Find(&posts).Error; err != nil {
        return nil, err
    }
    return &posts, nil
}

// IsFollowedByUser func
func (u *User) IsFollowedByUser(username string) bool {
    user, _ := GetUserByUsername(username)
    ids := user.FollowingIDs()
    for _, id := range ids {
        if u.ID == id {
            return true
        }
    }
    return false
}

// CreatePost func
func (u *User) CreatePost(body string) error {
    post := Post{Body: body, UserID: u.ID}
    return db.Create(&post).Error
}
...
```

然后在 AddUser 的时候加入 FollowSelf, 即自己关注自己，方便在显示 Profile 页的时候，展示 FollowingPosts 将自己的 Post 也列进去

model/user.go

```go
// AddUser func
func AddUser(username, password, email string) error {
    user := User{Username: username, Email: email}
    user.SetPassword(password)
    user.SetAvatar(email)
    if err := db.Create(&user).Error; err != nil {
        return err
    }
    return user.FollowSelf()
}
```

由于我们一开始的建立用户，没有加入 Follower 功能，我们重新初始化一遍数据

cmd/init\_db\main.go

```go
package main

import (
    "log"

    "github.com/bonfy/go-mega-code/model"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

func main() {
    log.Println("DB Init ...")
    db := model.ConnectToDB()
    defer db.Close()
    model.SetDB(db)

    db.DropTableIfExists(model.User{}, model.Post{}, "follower")
    db.CreateTable(model.User{}, model.Post{})

    model.AddUser("bonfy", "abc123", "i@bonfy.im")
    model.AddUser("rene", "abc123", "rene@test.com")

    u1, _ := model.GetUserByUsername("bonfy")
    u1.CreatePost("Beautiful day in Portland!")
    model.UpdateAboutMe(u1.Username, `I'm the author of Go-Mega Tutorial you are reading now!`)

    u2, _ := model.GetUserByUsername("rene")
    u2.CreatePost("The Avengers movie was so cool!")
    u2.CreatePost("Sun shine is beautiful")

    u1.Follow(u2.Username)
}
```

执行

```
$ go run cmd/db_init/main.go
```

然后查看 `follower` 表，里面就有数据了

![08-01](/files/-LO2UlexAX9hTbIOxxiX)

> 说明: 如上图 user\_id 表示用户， follower\_id 表示关注者， bonfy（id:1) 关注者 只有自己, 而 rene(id:2) 关注者 有两位, 这也是 u1.Follow(u2.Username) 的执行结果
>
> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/ef4e3aaab2a4ba077edf687e6af7dc2a907df3db)

## 完善页面显示

### 增加 Follow and Unfollow 操作

controller/home.go

```go
...

func (h home) registerRoutes() {
    r := mux.NewRouter()
    r.HandleFunc("/logout", middleAuth(logoutHandler))
    r.HandleFunc("/login", loginHandler)
    r.HandleFunc("/register", registerHandler)
    r.HandleFunc("/user/{username}", middleAuth(profileHandler))
    r.HandleFunc("/follow/{username}", middleAuth(followHandler))
    r.HandleFunc("/unfollow/{username}", middleAuth(unFollowHandler))
    r.HandleFunc("/profile_edit", middleAuth(profileEditHandler))
    r.HandleFunc("/", middleAuth(indexHandler))

    http.Handle("/", r)
}

...

func followHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    pUser := vars["username"]
    sUser, _ := getSessionUser(r)

    err := vm.Follow(sUser, pUser)
    if err != nil {
        log.Println("Follow error:", err)
        w.Write([]byte("Error in Follow"))
        return
    }
    http.Redirect(w, r, fmt.Sprintf("/user/%s", pUser), http.StatusSeeOther)
}

func unFollowHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    pUser := vars["username"]
    sUser, _ := getSessionUser(r)

    err := vm.UnFollow(sUser, pUser)
    if err != nil {
        log.Println("UnFollow error:", err)
        w.Write([]byte("Error in UnFollow"))
        return
    }
    http.Redirect(w, r, fmt.Sprintf("/user/%s", pUser), http.StatusSeeOther)
}
```

`Follow` 和 `Unfollow`之后通过 Redirect 回到原来的页面

### 完善Profile页面

vm/profile.go

```go
package vm

import "github.com/bonfy/go-mega-code/model"

// ProfileViewModel struct
type ProfileViewModel struct {
    BaseViewModel
    Posts          []model.Post
    Editable       bool
    IsFollow       bool
    FollowersCount int
    FollowingCount int
    ProfileUser    model.User
}

// ProfileViewModelOp struct
type ProfileViewModelOp struct{}

// GetVM func
func (ProfileViewModelOp) GetVM(sUser, pUser string) (ProfileViewModel, error) {
    v := ProfileViewModel{}
    v.SetTitle("Profile")
    u, err := model.GetUserByUsername(pUser)
    if err != nil {
        return v, err
    }
    posts, _ := model.GetPostsByUserID(u.ID)
    v.ProfileUser = *u
    v.Editable = (sUser == pUser)

    if !v.Editable {
        v.IsFollow = u.IsFollowedByUser(sUser)
    }
    v.FollowersCount = u.FollowersCount()
    v.FollowingCount = u.FollowingCount()

    v.Posts = *posts
    v.SetCurrentUser(sUser)
    return v, nil
}

// Follow func : A follow B
func Follow(a, b string) error {
    u, err := model.GetUserByUsername(a)
    if err != nil {
        return err
    }
    return u.Follow(b)
}

// UnFollow func : A unfollow B
func UnFollow(a, b string) error {
    u, err := model.GetUserByUsername(a)
    if err != nil {
        return err
    }
    return u.Unfollow(b)
}
```

templates/content/profile.html

```markup
...

    {{if .ProfileUser.LastSeen}}
        <p>Last seen on: {{ .ProfileUser.LastSeen }}</p>
    {{end}}

    <p>{{ .FollowersCount }} followers, {{ .FollowingCount }} following.</p>

    {{if .Editable}}
        <p><a href="/profile_edit">Edit your profile</a></p>
    {{else}}
        {{if .IsFollow}}
            <p><a href="/unfollow/{{.ProfileUser.Username}}">Unfollow</a></p>
        {{else}}
            <p><a href="/follow/{{.ProfileUser.Username}}">Follow</a></p>
        {{end}}
    {{end}}
...
```

运行

```
$ go run main.go
```

![08-02](/files/-LO2Ulf2w4gEVJ1R9WSI)

> 说明: 这个是通过 rene 登陆之后访问 `http://127.0.0.1:8888/user/bonfy` 查看 bonfy 的profile, 由于 rene 没有 follow bonfy，可以点击 `Follow` 按钮，实现Follow操作
>
> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/b1502cf92a9e1ee9f77cfa2b661646e6e4fd414c)

## Links

* [目录](/)
* 上一节: [07-Profile-Page-And-Avatar](/07-profile-page-and-avatar)
* 下一节: [09-Pagination](/09-pagination)


# 09-Pagination

在本章，我将告诉你如何对数据列表进行分页。

在[第八章](/08-follower)我们支持了社交网络非常流行的“粉丝”机制。 有了这个功能，接下来我准备好删除一开始就使用的模拟用户动态了。 在本章中，应用将开始接受来自用户的动态更新，并将其发布到网站首页和个人主页。

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/09-Pagination), [Diff](https://github.com/bonfy/go-mega-code/compare/08-Follower...09-Pagination), [Zip](https://github.com/bonfy/go-mega-code/archive/v0.9.zip)

## 发布用户动态

简言之，就是发布Post，首页需要有一个表单，用户可以在其中键入新动态。

不过在这之前，我们先支持下 `flash message`

controller/g.go

```go
...
var (
    homeController home
    templates      map[string]*template.Template
    sessionName    string
    flashName      string
    store          *sessions.CookieStore
)

func init() {
    templates = PopulateTemplates()
    store = sessions.NewCookieStore([]byte("something-very-secret"))
    sessionName = "go-mega"
    flashName = "go-flash"
}
...
```

controller/utils.go

```go
...

func setFlash(w http.ResponseWriter, r *http.Request, message string) {
    session, _ := store.Get(r, sessionName)
    session.AddFlash(message, flashName)
    session.Save(r, w)
}

func getFlash(w http.ResponseWriter, r *http.Request) string {
    session, _ := store.Get(r, sessionName)
    fm := session.Flashes(flashName)
    if fm == nil {
        return ""
    }
    session.Save(r, w)
    return fmt.Sprintf("%v", fm[0])
}
```

然后我们就可以使用 `flash message` 来提示 error message了

现在我们来完成发布Post功能

vm/index.go

```go
package vm

import "github.com/bonfy/go-mega-code/model"

// IndexViewModel struct
type IndexViewModel struct {
    BaseViewModel
    Posts []model.Post
    Flash string
}

// IndexViewModelOp struct
type IndexViewModelOp struct{}

// GetVM func
func (IndexViewModelOp) GetVM(username string, flash string) IndexViewModel {
    u, _ := model.GetUserByUsername(username)
    posts, _ := u.FollowingPosts()
    v := IndexViewModel{BaseViewModel{Title: "Homepage"}, *posts, flash}
    v.SetCurrentUser(username)
    return v
}

// CreatePost func
func CreatePost(username, post string) error {
    u, _ := model.GetUserByUsername(username)
    return u.CreatePost(post)
}
```

> Notice: 这里我们顺便将 IndexView 里的 Posts 改成 CurrentUser 的 FollowingPosts

templates/content/index.html

```markup
{{define "content"}}
    <h1>Hello, {{.CurrentUser}}!</h1>

    <form action="/" method="post">

        <p><textarea  name="body" rows="3" cols="80" value="" placeholder="say something..."></textarea></p>
        <p><input type="submit" name="submit" value="Post"></p>

        {{ if .Flash }}
            <span style="color: red;">[{{.Flash}}]</span>
        {{ end }}
    </form>


    {{range .Posts}}
        <table>
            <tr valign="top">
                <td><img src="{{.User.Avatar}}&s=36"></td>
                <td>{{ .User.Username }} says:<br>{{ .Body }}</td>
            </tr>
        </table>
    {{end}}
{{end}}
```

controller/home.go

```go
...
func indexHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "index.html"
    vop := vm.IndexViewModelOp{}
    username, _ := getSessionUser(r)
    if r.Method == http.MethodGet {
        flash := getFlash(w, r)
        v := vop.GetVM(username, flash)
        templates[tpName].Execute(w, &v)
    }
    if r.Method == http.MethodPost {
        r.ParseForm()
        body := r.Form.Get("body")
        errMessage := checkLen("Post", body, 1, 180)
        if errMessage != "" {
            setFlash(w, r, errMessage)
        } else {
            err := vm.CreatePost(username, body)
            if err != nil {
                log.Println("add Post error:", err)
                w.Write([]byte("Error insert Post in database"))
                return
            }
        }
        http.Redirect(w, r, "/", http.StatusSeeOther)
    }
}
...
```

![09-01](/files/-LOHg9J1w7cSqXbRD67d)

> Notice: 这里由于我们上章初始化数据 bonfy follow了 rene，所以这里 bonfy 看到的 Index 页面中也有rene的Post,如果登陆rene的账户，是看不到bonfy的Post的，因为 rene 没有follow bonfy

我们现在在输入框中什么都不输入，直接点Post,就能看到 Flash 的 红色提示了

![09-02](/files/-LOHg9J3KIpQWYd5lOk9)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/f1062528991817ed299dd4536bbfa2defa600c4f)

## 加入动态分页

应用看起来更完善了，但是在主页显示所有用户动态迟早会出问题。如果一个用户有成千上万条关注的用户动态时，会发生什么？你可以想象得到，管理这么大的用户动态列表将会变得相当缓慢和低效。

为了解决这个问题，我会将用户动态进行分页。这意味着一开始显示的只是所有用户动态的一部分，并提供链接来访问其余的用户动态。

我们先在 `controller/g.go` 中增加页数设置 `pageLimit` (其实更灵活点，也可以将它放入到配置文件中)

controller/g.go

```go
...
var (
    homeController home
    templates      map[string]*template.Template
    sessionName    string
    flashName      string
    store          *sessions.CookieStore
    pageLimit      int
)

func init() {
    templates = PopulateTemplates()
    store = sessions.NewCookieStore([]byte("something-very-secret"))
    sessionName = "go-mega"
    flashName = "go-flash"
    pageLimit = 5
}
...
```

接下来，我需要决定如何将页码并入到应用URL中。 一个相当常见的方法是使用查询字符串参数来指定一个可选的页码，如果没有给出则默认为页面1。 以下是一些示例网址，显示了我将如何实现这一点：

* 第1页，隐含：<http://localhost:8888/>
* 第1页，显式：<http://localhost:8888/?page=1>
* 第3页：<http://localhost:8888/?page=3>

要访问查询字符串中给出的参数,我们在 utils 中创建一个函数,方便以后调用

```go
...

func getPage(r *http.Request) int {
    url := r.URL         // net/url.URL
    query := url.Query() // Values (map[string][]string)

    q := query.Get("page")
    if q == "" {
        return 1
    }

    page, err := strconv.Atoi(q)
    if err != nil {
        return 1
    }
    return page
}
```

在 vm 中建立分页的 BasePageViewModel

* PrevPage: 上一页的页码&#x20;
* NextPage: 下一页的页码
* Total: 总页数
* CurrentPage: 当前页码
* Limit: 每页显示项目数

vm/g.go

```go
...

// BasePageViewModel struct
type BasePageViewModel struct {
    PrevPage    int
    NextPage    int
    Total       int
    CurrentPage int
    Limit       int
}

// SetPrevAndNextPage func
func (v *BasePageViewModel) SetPrevAndNextPage() {
    if v.CurrentPage > 1 {
        v.PrevPage = v.CurrentPage - 1
    }

    if (v.Total-1)/v.Limit >= v.CurrentPage {
        v.NextPage = v.CurrentPage + 1
    }
}

// SetBasePageViewModel func
func (v *BasePageViewModel) SetBasePageViewModel(total, page, limit int) {
    v.Total = total
    v.CurrentPage = page
    v.Limit = limit
    v.SetPrevAndNextPage()
}
```

### 首页中的分页

model 中增加 FollowingPosts 的分页处理

model/user.go

```go
...
// FollowingPostsByPageAndLimit func
func (u *User) FollowingPostsByPageAndLimit(page, limit int) (*[]Post, int, error) {
    var total int
    var posts []Post
    offset := (page - 1) * limit
    ids := u.FollowingIDs()
    if err := db.Preload("User").Order("timestamp desc").Where("user_id in (?)", ids).Offset(offset).Limit(limit).Find(&posts).Error; err != nil {
        return nil, total, err
    }
    db.Model(&Post{}).Where("user_id in (?)", ids).Count(&total)
    return &posts, total, nil
}
...
```

index 的 vm 中加入 BasePageViewModel

vm/index.go

```go
...
type IndexViewModel struct {
    BaseViewModel
    Posts []model.Post
    Flash string

    BasePageViewModel
}

// IndexViewModelOp struct
type IndexViewModelOp struct{}

// GetVM func
func (IndexViewModelOp) GetVM(username, flash string, page, limit int) IndexViewModel {
    u, _ := model.GetUserByUsername(username)
    posts, total, _ := u.FollowingPostsByPageAndLimit(page, limit)
    v := IndexViewModel{}
    v.SetTitle("Homepage")
    v.Posts = *posts
    v.Flash = flash
    v.SetBasePageViewModel(total, page, limit)
    v.SetCurrentUser(username)
    return v
}
...
```

显示页中加入页码

templates/content/index.html

```markup
...
    {{range .Posts}}
        <table>
            <tr valign="top">
                <td><img src="{{.User.Avatar}}&s=36"></td>
                <td><a href="/user/{{.User.Username}}">{{ .User.Username }}</a> says:<br>{{ .Body }}</td>
            </tr>
        </table>
    {{end}}

    {{ if gt .PrevPage 0 }}
        <a href="/?page={{.PrevPage}}">Newer posts</a>
    {{ end }}
    {{ if gt .NextPage 0 }}
        <a href="/?page={{.NextPage}}">Older posts</a>
    {{ end }}
...
```

> Notice: 这里我们顺便在 Posts显示的时候将 Username 上加了 “/user/username” 的链接,方便我们快速访问用户profile

controller修改成新的 GetVM

```go
...
func indexHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "index.html"
    vop := vm.IndexViewModelOp{}
    page := getPage(r)
    username, _ := getSessionUser(r)
    if r.Method == http.MethodGet {
        flash := getFlash(w, r)
        v := vop.GetVM(username, flash, page, pageLimit)
        templates[tpName].Execute(w, &v)
    }
    if r.Method == http.MethodPost {
        r.ParseForm()
        body := r.Form.Get("body")
        errMessage := checkLen("Post", body, 1, 180)
        if errMessage != "" {
            setFlash(w, r, errMessage)
        } else {
            err := vm.CreatePost(username, body)
            if err != nil {
                log.Println("add Post error:", err)
                w.Write([]byte("Error insert Post in database"))
                return
            }
        }
        http.Redirect(w, r, "/", http.StatusSeeOther)
    }
}
...
```

![09-03](/files/-LOHg9JB1HKigfYrhpKU)

### 个人主页中的分页

与首页分页类似，建立 profile 中的分页

model/post.go

```go
...
// GetPostsByUserIDPageAndLimit func
func GetPostsByUserIDPageAndLimit(id, page, limit int) (*[]Post, int, error) {
    var total int
    var posts []Post
    offset := (page - 1) * limit
    if err := db.Preload("User").Order("timestamp desc").Where("user_id=?", id).Offset(offset).Limit(limit).Find(&posts).Error; err != nil {
        return nil, total, err
    }
    db.Model(&Post{}).Where("user_id=?", id).Count(&total)
    return &posts, total, nil
}
```

vm/profile.go

```go
...
// ProfileViewModel struct
type ProfileViewModel struct {
    BaseViewModel
    Posts          []model.Post
    Editable       bool
    IsFollow       bool
    FollowersCount int
    FollowingCount int
    ProfileUser    model.User
    BasePageViewModel
}

// ProfileViewModelOp struct
type ProfileViewModelOp struct{}

// GetVM func
func (ProfileViewModelOp) GetVM(sUser, pUser string, page, limit int) (ProfileViewModel, error) {
    v := ProfileViewModel{}
    v.SetTitle("Profile")
    u, err := model.GetUserByUsername(pUser)
    if err != nil {
        return v, err
    }
    posts, total, _ := model.GetPostsByUserIDPageAndLimit(u.ID, page, limit)
    v.ProfileUser = *u
    v.Editable = (sUser == pUser)
    v.SetBasePageViewModel(total, page, limit)
    if !v.Editable {
        v.IsFollow = u.IsFollowedByUser(sUser)
    }
    v.FollowersCount = u.FollowersCount()
    v.FollowingCount = u.FollowingCount()

    v.Posts = *posts
    v.SetCurrentUser(sUser)
    return v, nil
}
...
```

templates/content/profile.html

```markup
...

    {{range .Posts}}
        <table>
            <tr valign="top">
                <td><img src="{{.User.Avatar}}&s=36"></td>
                <td><a href="/user/{{.User.Username}}">{{ .User.Username }}</a> says:<br>{{ .Body }}</td>
            </tr>
        </table>
    {{end}}

    {{ if gt .PrevPage 0 }}
        <a href="/user/{{.ProfileUser.Username}}?page={{.PrevPage}}">Newer posts</a>
    {{ end }}
    {{ if gt .NextPage 0 }}
        <a href="/user/{{.ProfileUser.Username}}?page={{.NextPage}}">Older posts</a>
    {{ end }}
...
```

controller/home.go

```go
...
func profileHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "profile.html"
    vars := mux.Vars(r)
    pUser := vars["username"]
    sUser, _ := getSessionUser(r)
    page := getPage(r)
    vop := vm.ProfileViewModelOp{}
    v, err := vop.GetVM(sUser, pUser, page, pageLimit)
    if err != nil {
        msg := fmt.Sprintf("user ( %s ) does not exist", pUser)
        w.Write([]byte(msg))
        return
    }
    templates[tpName].Execute(w, &v)
}
...
```

![09-04](/files/-LOHg9JEdx1RGFAXBFFh)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/fd3c82514e78098e0f482f2be8c0797a0e6cef67)

## 更容易地发现和关注用户

相信你已经留意到了，应用没有一个很好的途径来让用户可以找到其他用户进行关注。实际上，现在根本没有办法在页面上查看到底有哪些用户存在。我将会使用少量简单的变更来解决这个问题。

我将会创建一个新的`Explore`页面。该页面看起来像是主页，但是却不是只显示已关注用户的动态，而是展示所有用户的全部动态。

我们现在导航中加入`Explore`

templates/\_base.html

```markup
...
        <a href="/">Home</a>
        <a href="/explore">Explore</a>
...
```

然后增加 `Explore` 页面， 不多说了，老套路

model/post.go

```go
...
// GetPostsByPageAndLimit func
func GetPostsByPageAndLimit(page, limit int) (*[]Post, int, error) {
    var total int
    var posts []Post

    offset := (page - 1) * limit
    if err := db.Preload("User").Offset(offset).Limit(limit).Order("timestamp desc").Find(&posts).Error; err != nil {
        return nil, total, err
    }

    db.Model(&Post{}).Count(&total)

    return &posts, total, nil
}
```

vm/explore.go

```go
package vm

import "github.com/bonfy/go-mega-code/model"

// ExploreViewModel struct
type ExploreViewModel struct {
    BaseViewModel
    Posts []model.Post
    BasePageViewModel
}

// ExploreViewModelOp struct
type ExploreViewModelOp struct{}

// GetVM func
func (ExploreViewModelOp) GetVM(username string, page, limit int) ExploreViewModel {
    // posts, _ := model.GetAllPosts()
    posts, total, _ := model.GetPostsByPageAndLimit(page, limit)
    v := ExploreViewModel{}
    v.SetTitle("Explore")
    v.Posts = *posts
    v.SetBasePageViewModel(total, page, limit)
    v.SetCurrentUser(username)
    return v
}
```

templates/content/explore.html

```markup
{{define "content"}}
    <h1>Hello, {{.CurrentUser}}!</h1>

    {{range .Posts}}
        <table>
            <tr valign="top">
                <td><img src="{{.User.Avatar}}&s=36"></td>
                <td><a href="/user/{{.User.Username}}">{{ .User.Username }}</a> says:<br>{{ .Body }}</td>
            </tr>
        </table>
    {{end}}

    {{ if gt .PrevPage 0 }}
        <a href="/explore?page={{.PrevPage}}">Newer posts</a>
    {{ end }}
    {{ if gt .NextPage 0 }}
        <a href="/explore?page={{.NextPage}}">Older posts</a>
    {{ end }}
{{end}}
```

controller/home.go

```go
...
func (h home) registerRoutes() {
    r := mux.NewRouter()
    r.HandleFunc("/logout", middleAuth(logoutHandler))
    r.HandleFunc("/login", loginHandler)
    r.HandleFunc("/register", registerHandler)
    r.HandleFunc("/user/{username}", middleAuth(profileHandler))
    r.HandleFunc("/follow/{username}", middleAuth(followHandler))
    r.HandleFunc("/unfollow/{username}", middleAuth(unFollowHandler))
    r.HandleFunc("/profile_edit", middleAuth(profileEditHandler))
    r.HandleFunc("/explore", middleAuth(exploreHandler))
    r.HandleFunc("/", middleAuth(indexHandler))

    http.Handle("/", r)
}

...

func exploreHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "explore.html"
    vop := vm.ExploreViewModelOp{}
    username, _ := getSessionUser(r)
    page := getPage(r)
    v := vop.GetVM(username, page, pageLimit)
    templates[tpName].Execute(w, &v)
}
```

通过这些细小的变更，应用的用户体验得到了大大的提升。现在，用户可以访问发现页来查看陌生用户的动态，并通过这些用户动态来关注用户，而需要的操作仅仅是点击用户名跳转到其个人主页并点击关注链接。令人叹为观止！对吧？

此时，我建议你在应用上再次尝试一下这个功能，以便体验最后的用户接口的完善。

![09-05](/files/-LOHg9JJgh6un0SJYo76)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/4609f2bd8bf293ccd2dc1871a03e7e8c91d44706)

## Links

* [目录](/)
* 上一节: [08-Follower](/08-follower)
* 下一节: [10-Email-Support](/10-email-support)


# 10-Email-Support

这是Go-Mega系列的第十部分，本章我将告诉你，应用如何向你的用户发送电子邮件，以及如何在电子邮件支持之上构建密码重置功能。

现在，应用在数据库方面做得相当不错，所以在本章中，我想抛开这个主题，开始添加发送电子邮件的功能，这是大多数Web应用必需的另一个重要部分。

为什么应用需要发送电子邮件给用户？ 原因很多，但其中一个常见的原因是解决与认证相关的问题。 在本章中，我将为忘记密码的用户添加密码重置功能。 当用户请求重置密码时，应用将发送包含特制链接的电子邮件。 用户然后需要点击该链接才能访问设置新密码的表单。

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/10-Email-Support), [Diff](https://github.com/bonfy/go-mega-code/compare/09-Pagination...10-Email-Support), [Zip](https://github.com/bonfy/go-mega-code/archive/v1.0.zip)

## 第三方库支持

本章我们需要两个第三方插件 `gomail` 以及 `jwt-go`

```
# gomail
$ go get gopkg.in/gomail.v2

# jwt-go
$ go get github.com/dgrijalva/jwt-go
```

## 加入mail支持

在 config 中增加 mail 设置

config.yml

```
mysql:
  charset: utf8
  db: dbname
  host: localhost
  password: password
  user: root
mail:
  smtp: smtp-server
  smtp-port: 587
  user: user
  password: pwd
```

这里的 smtp server， 请查看你的邮件提供商的文档，比如 zoho mail 的 smtp 是 `smtp.zoho.com`

config/g.go

```go
...

// GetSMTPConfig func
func GetSMTPConfig() (server string, port int, user, pwd string) {
    server = viper.GetString("mail.smtp")
    port = viper.GetInt("mail.smtp-port")
    user = viper.GetString("mail.user")
    pwd = viper.GetString("mail.password")
    return
}
```

在 controller/utils.go 封装 sendMail 函数，方便调用

controller/utils.go

```go
...


// Email

// sendEmail func
func sendEmail(target, subject, content string) {
    server, port, usr, pwd := config.GetSMTPConfig()
    d := gomail.NewDialer(server, port, usr, pwd)
    d.TLSConfig = &tls.Config{InsecureSkipVerify: true}

    m := gomail.NewMessage()
    m.SetHeader("From", usr)
    m.SetHeader("To", target)
    m.SetAddressHeader("Cc", usr, "admin")
    m.SetHeader("Subject", subject)
    m.SetBody("text/html", content)

    if err := d.DialAndSend(m); err != nil {
        log.Println("Email Error:", err)
        return
    }
}
```

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/f741e7875dba01644c7fe60faecc76c043fffe1e)

## 请求重置密码

我上面提到过，用户有权利重置密码。因此我将在登录页面提供一个链接：

templates/content/login.html

```markup
...
    <p>New User? <a href="/register">Click to Register!</a></p>
    <p>Forget Password? <a href="/reset_password_request">Click to Reset Password!</a></p>
...
```

![10-01](/files/-LOWzqVfEqic0ZNcY1Jy)

当用户点击链接时，会出现一个新的Web表单，要求用户输入注册的电子邮件地址，以启动密码重置过程。

vm/reset\_password\_request.go

```go
package vm

import (
    "log"

    "github.com/bonfy/go-mega-code/model"
)

// ResetPasswordRequestViewModel struct
type ResetPasswordRequestViewModel struct {
    LoginViewModel
}

// ResetPasswordRequestViewModelOp struct
type ResetPasswordRequestViewModelOp struct{}

// GetVM func
func (ResetPasswordRequestViewModelOp) GetVM() ResetPasswordRequestViewModel {
    v := ResetPasswordRequestViewModel{}
    v.SetTitle("Forget Password")
    return v
}

// CheckEmailExist func
func CheckEmailExist(email string) bool {
    _, err := model.GetUserByEmail(email)
    if err != nil {
        log.Println("Can not find email:", email)
        return false
    }
    return true
}
```

templates/content/reset\_password\_request.html

```markup
{{define "content"}}
    <h1>Input your email address:</h1>
    <form action="/reset_password_request" method="post" name="reset_password_request">
        <p><input type="text" class="form-control" name="email" value="" placeholder="Email"></p>
        <p><input type="submit" class="btn btn-outline-primary" name="submit" value="Submit" ></p>
    </form>

    {{if .Errs}}
    <ul>
        {{range .Errs}}
            <li>{{.}}</li>
         {{end}}
    </ul>
    {{end}}
{{end}}
```

controller/home.go

```go
...

r.HandleFunc("/reset_password_request", resetPasswordRequestHandler)
...

func resetPasswordRequestHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "reset_password_request.html"
    vop := vm.ResetPasswordRequestViewModelOp{}
    v := vop.GetVM()

    if r.Method == http.MethodGet {
        templates[tpName].Execute(w, &v)
    }
    if r.Method == http.MethodPost {
        r.ParseForm()
        email := r.Form.Get("email")

        errs := checkResetPasswordRequest(email)
        v.AddError(errs...)

        if len(v.Errs) > 0 {
            templates[tpName].Execute(w, &v)

        } else {
            log.Println("Send mail to", email)
            vopEmail := vm.EmailViewModelOp{}
            vEmail := vopEmail.GetVM(email)
            var contentByte bytes.Buffer
            tpl, _ := template.ParseFiles("templates/email.html")

            if err := tpl.Execute(&contentByte, &vEmail); err != nil {
                log.Println("Get Parse Template:", err)
                w.Write([]byte("Error send email"))
                return
            }
            content := contentByte.String()
            go sendEmail(email, "Reset Password", content)
            http.Redirect(w, r, "/login", http.StatusSeeOther)
        }
    }
}
```

![10-02](/files/-LOWzqVkw0UtIetTALxC)

controller 里涉及到了 email 的template

vm/email.go

```go
package vm

import (
    "github.com/bonfy/go-mega-code/config"
    "github.com/bonfy/go-mega-code/model"
)

// EmailViewModel struct
type EmailViewModel struct {
    Username string
    Token    string
    Server   string
}

// EmailViewModelOp struct
type EmailViewModelOp struct{}

// GetVM func
func (EmailViewModelOp) GetVM(email string) EmailViewModel {
    v := EmailViewModel{}
    u, _ := model.GetUserByEmail(email)
    v.Username = u.Username
    v.Token, _ = u.GenerateToken()
    v.Server = config.GetServerURL()
    return v
}
```

templates/email.html

```markup
<p>Dear {{.Username}},</p>
<p>
    To reset your password
    <a href="{{.Server}}/reset_password/{{.Token}}">
        click here
    </a>.
</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{.Server}}/reset_password/{{.Token}}</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>This Email will expire in 2 hours.</p>
<p>Sincerely,</p>
<p>BONFY</p>
```

![10-03](/files/-LOWzqVqGWIF9PVGHWG4)

上面简单的一封邮件，其实蕴含着两个 非常重要的 知识点：

### goroutine

原来 Flask-Mega 里面非常复杂的多线程操作，这里只用了一个 `go function()` 完成了

goroutine 可以说是 `Go` 这个语言的特色之一了，当然资料也比较多，大家可以深入了解下

这里我简单说下，就是 function 前面加个 go 关键字，就实现了 协程，用于高并发，而且性能非常好，是不是很cool！

### jwt

具体可以通过 [jwt.io](https://jwt.io/)了解下

或者中文的可以通过这片文章具体了解下 [直通车](https://juejin.im/post/5a437441f265da43294e54c3)

这里的邮件可以说是 JWT 的一个非常典型的应用场景，jwt 加密后的 URL 就是图中的一长串，其中其实隐含着 2 hour 过期时间

我们通过在 user.go 里加入两个function 就能实现, 密钥 `secret` 这里直接写在代码里，其实更优还是通过配置文件配置，又偷懒了

model/user.go

```go
...

// GenerateToken func
func (u *User) GenerateToken() (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "username": u.Username,
        "exp":      time.Now().Add(time.Hour * 2).Unix(), // 可以添加过期时间
    })
    return token.SignedString([]byte("secret"))
}

// CheckToken func
func CheckToken(tokenString string) (string, error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // Don't forget to validate the alg is what you expect:
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }

        // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
        return []byte("secret"), nil
    })

    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        return claims["username"].(string), nil
    } else {
        return "", err
    }
}

...
```

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/3e6471f01981dc176ee588579aa02aae4b6ad683)

## 重置密码

目前我们点击邮件中的邮件是不能操作的，因为我们其实并没有 `/reset_password` 这个handler，那这里我们来完成它

其实密码重置就是一个简单的表单， 两个密码框，检验输入一致以及符合密码规范就可以了，操作起来不算太复杂。

vm/reset\_password.go

```go
package vm

import (
    "github.com/bonfy/go-mega-code/model"
)

// ResetPasswordViewModel struct
type ResetPasswordViewModel struct {
    LoginViewModel
    Token string
}

// ResetPasswordViewModelOp struct
type ResetPasswordViewModelOp struct{}

// GetVM func
func (ResetPasswordViewModelOp) GetVM(token string) ResetPasswordViewModel {
    v := ResetPasswordViewModel{}
    v.SetTitle("Reset Password")
    v.Token = token
    return v
}

// CheckToken func
func CheckToken(tokenString string) (string, error) {
    return model.CheckToken(tokenString)
}

// ResetUserPassword func
func ResetUserPassword(username, password string) error {
    return model.UpdatePassword(username, password)
}
```

templates/content/reset\_password.html

```markup
{{define "content"}}
    <h1>Register</h1>
    <form action="/reset_password/{{.Token}}" method="post" name="reset_password">
        <p><input type="password" name="pwd1" value="" placeholder="Password"></p>
        <p><input type="password" name="pwd2" value="" placeholder="Confirm Password"></p>
        <p><input type="submit" name="submit" value="Reset"></p>
    </form>


    {{if .Errs}}
    <ul>
        {{range .Errs}}
            <li>{{.}}</li>
         {{end}}
    </ul>
    {{end}}
{{end}}
```

controller/home.go

```go
...

r.HandleFunc("/reset_password/{token}", resetPasswordHandler)

...

func resetPasswordHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    token := vars["token"]
    username, err := vm.CheckToken(token)
    if err != nil {
        w.Write([]byte("The token is no longer valid, please go to the login page."))
    }

    tpName := "reset_password.html"
    vop := vm.ResetPasswordViewModelOp{}
    v := vop.GetVM(token)

    if r.Method == http.MethodGet {
        templates[tpName].Execute(w, &v)
    }

    if r.Method == http.MethodPost {
        log.Println("Reset password for ", username)
        r.ParseForm()
        pwd1 := r.Form.Get("pwd1")
        pwd2 := r.Form.Get("pwd2")

        errs := checkResetPassword(pwd1, pwd2)
        v.AddError(errs...)

        if len(v.Errs) > 0 {
            templates[tpName].Execute(w, &v)
        } else {
            if err := vm.ResetUserPassword(username, pwd1); err != nil {
                log.Println("reset User password error:", err)
                w.Write([]byte("Error update user password in database"))
                return
            }
            http.Redirect(w, r, "/login", http.StatusSeeOther)
        }
    }
}
```

![10-04](/files/-LOWzqVu-0FkP22Qhgzm)

就这样我们完成了重置密码的功能，虽然设计到的页面比较多，不过只要我们思路清晰，一步一个脚印，还是能非常容易就能实现了。

现在输入新的密码，保存后，就可以用新密码登陆了。

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/4bc0b9304d60db854cb11b8ba5c5e908566da9a6)
>
> Notice: 本章还涉及到一些后端验证，在这里没有一一列举，大家还是请看下源码diff，可以更完整的了解代码

## Links

* [目录](/)
* 上一节: [09-Pagination](/09-pagination)
* 下一节: [11-Facelift](/11-facelift)


# 11-Facelift

在本章中，我暂停一下后端的工作，并花点时间向你展示如何使应用看起来更加优雅和专业。

本章主要是用到了 前端的 Bootstrap

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/11-Facelift), [Diff](https://github.com/bonfy/go-mega-code/compare/10-Email-Support...11-Facelift), [Zip](https://github.com/bonfy/go-mega-code/archive/v1.1.zip)

## CSS 框架

虽然我们可以争辩说写代码不容易，但是与那些必须让网页在所有Web浏览器上具有良好一致外观的网页设计师相比，我们的痛苦不值一提。 虽然近年来这种情况得到一定程度的缓解，但是在一些浏览器中仍然存在着晦涩的错误或奇怪的设定，这使得设计网页的任务变得非常困难。 如果还需要兼容屏幕限制设备（诸如平板电脑和智能手机）的浏览器，则更加困难。

如果你和我一样，只是一个想创建出规范网页的开发人员，没有时间或兴趣去学习底层机制并通过编写原生HTML和CSS来实现它，那么唯一可行的解决方案是使用CSS框架来简化任务。 通过这种方式，你会失去一些创造性的自由，但另一方面，无需通过太多的功夫就可以让网页在所有浏览器中看起来都不错。 CSS框架为普通类型的用户界面元素提供了高级CSS类的集合，其中包含预定义样式。 大多数这样的框架还提供JavaScript插件，以实现不能纯粹使用HTML和CSS来完成的功能。

## Bootstrap简介

最受欢迎的CSS框架之一是由Twitter推出的[Bootstrap](http://getbootstrap.com/)。 如果你想看看这个框架可以设计的页面类型，文档有一些[示例](https://getbootstrap.com/docs/4.1/examples/)。

这些是使用Bootstrap来设置网页风格的一些好处：

* 在所有主流网页浏览器中都有相似的外观
* 自动处理PC桌面，平板电脑和手机屏幕尺寸
* 可定制的布局
* 精心设计的导航栏，表单，按钮，警示，弹出窗口等

## Bootstrap美化

由于我们所有的页面都是继承自 `templates/_base.html`，所以我们需要在 \_base.html 中引入 bootstrap

templates/\_base.html

```markup
<html>
    <head>
        {{if .Title}}
        <title>{{.Title}} - blog</title>
        {{else}}
        <title>Welcome to blog!</title>
        {{end}}

        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">

        <style>
            .container {
                max-width: 960px;
            }
        </style>

    </head>
    <body>
        <div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm">
            <h5 class="my-0 mr-md-auto font-weight-normal">Blog</h5>
            <nav class="my-2 my-md-0 mr-md-3">
                <a class="p-2 text-dark" href="/">Home</a>
                <a class="p-2 text-dark" href="/explore">Explore</a>
                {{if .CurrentUser}}
                    <a class="p-2 text-dark" href="/user/{{.CurrentUser}}">Profile</a>
                {{end}}
            </nav>

            {{if .CurrentUser}}
                <a class="btn btn-outline-primary" href="/logout">Logout</a>
            {{else}}
                <a class="btn btn-outline-primary" href="/login">Login</a>
            {{end}}
        </div>

        <div class="container">
            {{template "content" .}}
        </div>

        <script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
    </body>
</html>
```

接着我们主要优化的是表单和按钮

* 所有表单上加上 `class="form-control"`
* 所有按钮上加上 `class="btn btn-outline-primary"`
* 所有的分页按钮优化

```markup
    <nav aria-label="...">
        <ul class="pagination justify-content-center">
        {{ if gt .PrevPage 0 }}
            <li class="page-item">
                <a href="/user/{{.ProfileUser.Username}}?page={{.PrevPage}}">
                    <span class="page-link" aria-hidden="true">&larr; Newer Posts</span>
                </a>
            </li>
        {{ else }}
            <li class="page-item disabled">
                <a href="#">
                    <span class="page-link" aria-hidden="true">&larr; Newer Posts</span>
                </a>
            </li> 
        {{ end }}
        {{ if gt .NextPage 0 }}
            <li class="page-item">
                <a href="/user/{{.ProfileUser.Username}}?page={{.NextPage}}">
                    <span class="page-link" aria-hidden="true">Older Posts &rarr;</span>
                </a>
            </li>
        {{ else }}
            <li class="page-item disabled">
                <a href="#">
                    <span class="page-link" aria-hidden="true">Older Posts &rarr;</span>
                </a>
            </li> 
        {{ end }}

        </ul>
    </nav>
```

![11-01](/files/-LOmydOPEHu3xiZIepWO)

![11-02](/files/-LOmydOR_iKLBqNVEWqz)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/ca5ccbaa06d9d4041357979246ed92c031ea722e)

## 自定义 404 界面

我们现在的404界面还是 `gorilla/mux` 的默认界面，现在我们美化下

vm/notfound.go

```go
package vm

// NotFoundMessage struct
type NotFoundMessage struct {
    Flash string
}
```

templates/404.html

```markup
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Page Not Found</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
            * {
                line-height: 1.2;
                margin: 0;
            }
            html {
                color: #888;
                display: table;
                font-family: sans-serif;
                height: 100%;
                text-align: center;
                width: 100%;
            }
            body {
                display: table-cell;
                vertical-align: middle;
                margin: 2em auto;
            }
            h1 {
                color: #555;
                font-size: 2em;
                font-weight: 400;
            }
            p {
                margin: 0 auto;
                width: 280px;
            }
            @media only screen and (max-width: 280px) {
                body, p {
                    width: 95%;
                }
                h1 {
                    font-size: 1.5em;
                    margin: 0 0 0.3em;
                }
            }
        </style>
    </head>
    <body>
        {{if .Flash}}
        <h1>{{.Flash}}</h1>
        {{else}}
        <h1>Page Not Found</h1>
        {{end}}
        <p>Sorry, but the page you were trying to view does not exist.</p>
    </body>
</html>
```

controller/home.go

```go
...
r.NotFoundHandler = http.HandlerFunc(notfoundHandler)
r.HandleFunc("/404", notfoundHandler)
...


func notfoundHandler(w http.ResponseWriter, r *http.Request) {
    flash := getFlash(w, r)
    message := vm.NotFoundMessage{Flash: flash}
    tpl, _ := template.ParseFiles("templates/404.html")
    tpl.Execute(w, &message)
}
...
```

运行程序，在浏览器中输入一个没有指定的 url，例如: `http://127.0.0.1:8888/nosuchpage`

![11-03](/files/-LOmydOVNT1ZBgk4MQMr)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/f90f32b1233c1e76dfbdba1c89649fbdd05d46cb)

## Links

* [目录](/)
* 上一节: [10-Email-Support](/10-email-support)
* 下一节: [12-Dates-And-Times](/12-dates-and-times)


# 12-Dates-And-Times

在本章，我们将讨论下 **Go** 处理日期和时间的方法，以及优化 User 及 Post 对象中的时间对象

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/12-Dates-And-Times), [Diff](https://github.com/bonfy/go-mega-code/compare/11-Facelift...12-Dates-And-Times), [Zip](https://github.com/bonfy/go-mega-code/archive/v1.2.zip)

## 优化 User 的 LastSeen

Flask-Mega 主要是用了它灵活的模板功能，以及 Moment.js 来在前端实现了时间日期的优化，与之不同，我们主要采用的是后端来实现同样的功能

model/user.go

```go
...
// FormattedLastSeen func
func (u *User) FormattedLastSeen() string {
    return u.LastSeen.Format("2006-01-02 15:04:05")
}
...
```

templates/content/profile.html

```markup
...
    <p>Last seen on: {{ .ProfileUser.FormattedLastSeen }}</p>
...
```

> Notice: 虽然我也比较喜欢其他语言的 %Y-%m-%d %H:%M:%S" layout 形式，但是无奈 Go 就是 "2006-01-02 15:04:05" 这样的layout，没有道理可讲，我有时候甚至怕它搞错 01 和 02 哪个是月 哪个表示 日

![12-01](/files/-LOzPnWhWVqQvkN4gWZ7)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/01611b3d27f6c78ca6c53878502f539695d1170b)

## 优化 Post 的 Timestamp

我们目标是显示成 `user said 35 minutes ago: post message` 这样的形式，这就需要我们对时间日期进行转换

简单点，我们就在 utils.go 中实现一个 `FromTime` 函数

model/utils.go

```go
...

const (
    minute  = 1
    hour    = minute * 60
    day     = hour * 24
    month   = day * 30
    year    = day * 365
    quarter = year / 4
)

// FromDuration returns a friendly string representing an approximation of the
// given duration
func FromDuration(d time.Duration) string {
    seconds := round(d.Seconds())

    if seconds < 30 {
        return "less than a minute"
    }

    if seconds < 90 {
        return "1 minute"
    }

    minutes := div(seconds, 60)

    if minutes < 45 {
        return fmt.Sprintf("%0d minutes", minutes)
    }

    hours := div(minutes, 60)

    if minutes < day {
        return fmt.Sprintf("about %s", pluralize(hours, "hour"))
    }

    if minutes < (42 * hour) {
        return "1 day"
    }

    days := div(hours, 24)

    if minutes < (30 * day) {
        return pluralize(days, "day")
    }

    months := div(days, 30)

    if minutes < (45 * day) {
        return "about 1 month"
    }

    if minutes < (60 * day) {
        return "about 2 months"
    }

    if minutes < year {
        return pluralize(months, "month")
    }

    rem := minutes % year
    years := minutes / year

    if rem < (3 * month) {
        return fmt.Sprintf("about %s", pluralize(years, "year"))
    }
    if rem < (9 * month) {
        return fmt.Sprintf("over %s", pluralize(years, "year"))
    }

    years++
    return fmt.Sprintf("almost %s", pluralize(years, "year"))
}

// FromTime returns a friendly string representing the approximate difference
// from the given time and time.Now()
func FromTime(t time.Time) string {
    now := time.Now()

    var d time.Duration
    var suffix string

    if t.Before(now) {
        d = now.Sub(t)
        suffix = "ago"
    } else {
        d = t.Sub(now)
        suffix = "from now"
    }

    return fmt.Sprintf("%s %s", FromDuration(d), suffix)
}

func pluralize(i int, s string) string {
    var buf bytes.Buffer
    buf.WriteString(fmt.Sprintf("%d %s", i, s))
    if i != 1 {
        buf.WriteString("s")
    }
    return buf.String()
}

func round(f float64) int {
    return int(math.Floor(f + .50))
}

func div(numerator int, denominator int) int {
    rem := numerator % denominator
    result := numerator / denominator

    if rem >= (denominator / 2) {
        result++
    }

    return result
}
```

现在我们可以使用 `FromTime` 函数实现我们的功能了

model/post.go

```go
...
// FormattedTimeAgo func
func (p *Post) FormattedTimeAgo() string {
    return FromTime(*p.Timestamp)
}
...
```

templates/content/index.html & profile.html & explore.html

```markup
...
    {{range .Posts}}
        <table class="table table-hover">
            <tr valign="top">
                <td width="36px"><img src="{{.User.Avatar}}&s=36"></td>
                <td><a href="/user/{{.User.Username}}">{{ .User.Username }}</a> said {{.FormattedTimeAgo}}:<br>{{ .Body }}</td>
            </tr>
        </table>
    {{end}}
...
```

![12-02](/files/-LOzPnWnABdwki6KRUOu)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/b5cdcf1fec212a012486928d2bc0ad3052d37d51)
>
> 写在最后: 突然发现模板可以使用 类的 Function, 所以前面 User 的 Avatar 字段貌似就没有那么必要了，直接用个function来实现，而且好处是不用去关心更改邮箱后的 SetAvatar,不过我任性，就不改了，我也是才发现 Go 模板还有这个妙用的

## Links

* [目录](/)
* 上一节: [11-Facelift](/11-facelift)
* 下一节: [13-Javascript-Magic](/13-javascript-magic)


# 13-Javascript-Magic

在本章，我们将添加一个功能，当你将鼠标悬停在用户的昵称上时，会弹出一个漂亮的窗口。

社交网站的常见用户交互模式是，当你将鼠标悬停在用户的名称上时，可以在弹出窗口中显示用户的主要信息。 如果你从未注意到这一点，请访问Twitter，Facebook，LinkedIn或任何其他主要社交网站，当你看到用户名时，只需将鼠标指针放在上面几秒钟即可看到弹出窗口。

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/13-Javascript-Magic), [Diff](https://github.com/bonfy/go-mega-code/compare/12-Dates-And-Times...13-Javascript-Magic), [Zip](https://github.com/bonfy/go-mega-code/archive/v1.3.zip)

## 服务器端支持

在深入研究客户端之前，让我们先了解一下支持这些用户弹窗所需的服务器端的工作。 用户弹窗的内容将由新路由返回，它是现有个人主页路由的简化版本。

viewmodel 我们偷下懒，由于Popup的 vm 和 Profile 的相似，我们直接在 `vm/profile.go`中加入 GetPopupVM 来获得 Popup 的 vm

vm/profile.go

```go
...
    // GetPopupVM func
func (ProfileViewModelOp) GetPopupVM(sUser, pUser string) (ProfileViewModel, error) {
    v := ProfileViewModel{}
    v.SetTitle("Profile")
    u, err := model.GetUserByUsername(pUser)
    if err != nil {
        return v, err
    }
    v.ProfileUser = *u
    v.Editable = (sUser == pUser)
    if !v.Editable {
        v.IsFollow = u.IsFollowedByUser(sUser)
    }
    v.FollowersCount = u.FollowersCount()
    v.FollowingCount = u.FollowingCount()
    v.SetCurrentUser(sUser)
    return v, nil
}

...
```

controller/home.go

```go
...
r.HandleFunc("/user/{username}/popup", popupHandler)

...
func popupHandler(w http.ResponseWriter, r *http.Request) {
    tpName := "popup.html"
    vars := mux.Vars(r)
    pUser := vars["username"]
    sUser, _ := getSessionUser(r)
    vop := vm.ProfileViewModelOp{}
    v, err := vop.GetPopupVM(sUser, pUser)
    if err != nil {
        msg := fmt.Sprintf("user ( %s ) does not exist", pUser)
        w.Write([]byte(msg))
        return
    }
    templates[tpName].Execute(w, &v)
}

...
```

templates/content/popup.html

```markup
<table>
    <tr valign="top">
        <td width="64" style="border: 0px;"><img src="{{.ProfileUser.Avatar}}&s=64"></td>
        <td style="border: 0px;">
            <small>
                <p><a href="/user/{{.ProfileUser.Username}}">{{.ProfileUser.Username}}</a></p>
                {{if .ProfileUser.AboutMe}}
                    <p>{{ .ProfileUser.AboutMe }}</p>
                {{end}}

                {{if .ProfileUser.LastSeen}}
                    <p>Last seen on: {{ .ProfileUser.FormattedLastSeen }}</p>
                {{end}}

                <p>{{ .FollowersCount }} followers, {{ .FollowingCount }} following.</p>

                {{if .Editable}}
                    <p><a href="/profile_edit">Edit your profile</a></p>
                {{else}}
                    {{if .IsFollow}}
                        <p><a  class="btn btn-outline-primary" href="/unfollow/{{.ProfileUser.Username}}">Unfollow</a></p>
                    {{else}}
                        <p><a  class="btn btn-outline-primary" href="/follow/{{.ProfileUser.Username}}">Follow</a></p>
                    {{end}}
                {{end}}
            </small>
        </td>
    </tr>
</table>
```

当用户将鼠标指针悬停在用户名上时，随后小节中编写的JavaScript代码将会调用此路由。客户端将服务器端返回的响应中的html内容显示在弹出窗口中。 当用户移开鼠标时，弹出窗口将被删除。 听起来很简单，对吧？

如果你想了解弹窗像什么样，现在可以运行应用，跳转到任何用户的个人主页，然后在地址栏的URL中追&#x52A0;*/popup*以查看全屏版本的弹出窗口内容。

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/f48f4a33425c1a10622ab721adee43053cf96ccc)

## 客户端Ajax

我们在 \_base.html 中加入 popup 的 Ajax,这样所有继承它的页面也同样继承了 popup的功能

templates/\_base.html

```markup
...
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
    <script>
        $(function () {
        var timer = null;
        var xhr = null;
        $('.user_popup').hover(
            function(event) {
                // mouse in event handler
                var elem = $(event.currentTarget);
                timer = setTimeout(function() {
                    timer = null;
                    xhr = $.ajax(
                        '/user/' + elem.first().text().trim() + '/popup').done(
                            function(data) {
                                xhr = null;
                                elem.popover({
                                    trigger: 'manual',
                                    html: true,
                                    animation: false,
                                    container: elem,
                                    content: data
                                }).popover('show');
                            }
                        );
                }, 1000);
            },
            function(event) {
                // mouse out event handler
                var elem = $(event.currentTarget);
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
                else if (xhr) {
                    xhr.abort();
                    xhr = null;
                }
                else {
                    elem.popover('hide');
                }
            }
        );
    });
    </script>

...
```

然后我们在需要有 Popup 功能的地方，就是所有的用户Post的头像地方加入 `class='user_popup'`

templates/content/index.html & explore.html & profile.html

```markup
...
    <td><span class="user_popup"><a href="/user/{{.User.Username}}">{{ .User.Username }}</a></span> said {{.FormattedTimeAgo}}:<br>{{ .Body }}</td>
...
```

![13-01](/files/-LPFA7FjttxwqoYBDzwh)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/8259a0b1347ea5aeab1b6b7351b88d147ce3c01b)

## Links

* [目录](/)
* 上一节: [12-Dates-And-Times](/12-dates-and-times)
* 下一节: [14-Deployment-On-Heroku](/14-deployment-on-heroku)


# 14-Deployment-On-Heroku

在本章，我会将应用部署到Heroku云平台

许多云托管提供商提供了一个应用程序可以运行的托管平台。 你只需提供部署到这些平台上的实际应用程序，因为硬件，操作系统，脚本语言解释器，数据库等都由该服务管理。 这种服务称为平台即服务（PaaS）。

[Heroku](https://www.heroku.com/),这是一种流行的云托管服务,对Python、Go、Nodejs等应用程序支持都很好,关键还免费,而且默认支持HTTPS

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/14-Deployment-On-Heroku), [Diff](https://github.com/bonfy/go-mega-code/compare/13-Javascript-Magic...14-Deployment-On-Heroku), [Zip](https://github.com/bonfy/go-mega-code/archive/v1.4.zip)

## Heroku

Heroku 是首批PaaS平台之一。 它以Ruby的应用程序的托管服务开始，随后逐渐发展到支持诸多其他语言，如Java，Node.js，Python 还有Go。

在 Heroku 中部署Web应用程序主要是通过git版本控制工具完成的，因此你必须将应用程序放在git代码库中。 在通过git将应用程序上传到 Heroku 的服务器之后，你的工作基本就完成了，只需等待几秒钟，应用程序就会上线。 整个操作流程就是这么简单。

Heroku提供不同的服务级别，允许你自主选择为应用程序提供多少计算能力和运行时间，随着用户群的增长，你需要购买更多的“dynos”计算单元。

## 创建Heroku账户

在部署应用到 Heroku 之前，你需要拥有一个帐户。 所以请访问 `heroku.com` 并创建一个免费账户。 一旦注册成功并登录到 Heroku，你将可以访问一个dashboard，其中列出了你的所有应用程序。

## 安装Heroku命令行工具

我们可以在 Heroku 的 dashboard 上完成所有操作，不过还有个更简便的方法，就是安装Heroku cli 工具

```
$ brew install heroku
```

然后我们可以通过命令行来登陆 Heroku

```
$ heroku login
```

## 创建Heroku应用

要用Heroku注册一个新应用，需要在应用程序根目录下使用`apps:create`子命令，并将应用程序名称作为唯一参数传递：

```
$ heroku apps:create go-mega
Creating ⬢ go-mega... done
https://go-mega.herokuapp.com/ | https://git.heroku.com/go-mega.git
```

## 创建 Heroku 数据库

Heroku 只有Postgres数据库是免费的，Mysql是收费的，所以我们还是创建 Postgres 数据库

```
$ heroku addons:add heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on ⬢ go-mega... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-angular-82467 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation
```

## 初始化数据

```
$ heroku config
=== go-mega Config Vars
DATABASE_URL: ******************
```

#### 针对Heroku做些代码优化

由于Heroku采用的是 Configvar 的设置环境变量的方式，而且我们把 config.yml git ignore了，所以加入 `os.Getenv` 的方式去获取Configvar的环境变量，包括 DBTYPE, EMAIL相关，以及Heroku postgres 提供的 DATABASE\_URL

config/g.go

```go
package config

import (
    "fmt"
    "log"
    "os"
    "strconv"

    "github.com/spf13/viper"
)

func init() {
    projectName := "go-mega"
    dbType := GetDBType()
    log.Println("OS DBTYPE:", dbType)

    if IsHeroku() {
        log.Println("Get Env from os.env")
    } else {
        log.Println("Init viper")
        getConfig(projectName)
    }
}

func getConfig(projectName string) {
    viper.SetConfigName("config") // name of config file (without extension)

    viper.AddConfigPath(".")                                                // optionally look for config in the working directory
    viper.AddConfigPath(fmt.Sprintf("$HOME/.%s", projectName))              // call multiple times to add many search paths
    viper.AddConfigPath(fmt.Sprintf("/data/docker/config/%s", projectName)) // path to look for the config file in

    err := viper.ReadInConfig() // Find and read the config file
    if err != nil {             // Handle errors reading the config file
        panic(fmt.Errorf("Fatal error config file: %s", err))
    }
}

// GetMysqlConnectingString func
func GetMysqlConnectingString() string {
    usr := viper.GetString("mysql.user")
    pwd := viper.GetString("mysql.password")
    host := viper.GetString("mysql.host")
    db := viper.GetString("mysql.db")
    charset := viper.GetString("mysql.charset")

    return fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=%s&parseTime=true&loc=Local", usr, pwd, host, db, charset)
}

// GetHerokuConnectingString func
func GetHerokuConnectingString() string {
    return os.Getenv("DATABASE_URL")
}

// GetSMTPConfig func
func GetSMTPConfig() (server string, port int, user, pwd string) {
    if IsHeroku() {
        server = os.Getenv("MAIL_SMTP")
        port, _ = strconv.Atoi(os.Getenv("MAIL_SMTP_PORT"))
        user = os.Getenv("MAIL_USER")
        pwd = os.Getenv("MAIL_PASSWORD")
        return
    }

    server = viper.GetString("mail.smtp")
    port = viper.GetInt("mail.smtp-port")
    user = viper.GetString("mail.user")
    pwd = viper.GetString("mail.password")
    return
}

// GetServerURL func
func GetServerURL() (url string) {
    if IsHeroku() {
        url = os.Getenv("SERVER_URL")
        return
    }
    url = viper.GetString("server.url")
    return
}

// GetDBType func
func GetDBType() string {
    dbtype := os.Getenv("DBTYPE")
    return dbtype
}

// IsHeroku func
func IsHeroku() bool {
    return GetDBType() == "heroku"
}
```

修改ConnectToDB函数，支持postgres的数据库形式

model/g.go

```go
...

// ConnectToDB func
func ConnectToDB() *gorm.DB {
    if config.IsHeroku() {
        return ConnectToDBByDBType("postgres", config.GetHerokuConnectingString())
    }
    return ConnectToDBByDBType("mysql", config.GetMysqlConnectingString())
}

// ConnectToDBByDBType func
func ConnectToDBByDBType(dbtype, connectingStr string) *gorm.DB {
    log.Println("DB Type:", dbtype, "\nConnet to db...")
    db, err := gorm.Open(dbtype, connectingStr)
    if err != nil {
        panic("Failed to connect database")
    }
    db.SingularTable(true)
    return db
}
```

cmd/db\_init/main.go

```go
    ...
    _ "github.com/jinzhu/gorm/dialects/postgres"
    ...
```

main.go 与数据初始化无关，不过最后部署还是要做响应的调整

main.go

```go
    ...
    _ "github.com/jinzhu/gorm/dialects/postgres"
    ...


    port := os.Getenv("PORT")
    log.Println("Running on port: ", port)
    http.ListenAndServe(":"+port, context.ClearHandler(http.DefaultServeMux))
```

### 数据init

```
$ export DATABASE_URL=postgres://xxxxxxx
$ export DBTYPE=heroku

$ go run cmd/db_init/main.go
```

然后我们会发现数据初始化就完成了，让我们确认下

```
# 安装 psql
$ brew install postgresql

$ heroku pg:psql 
$ DATABASE=> select * from post;
id | user_id |              body               |           timestamp
----+---------+---------------------------------+-------------------------------
  1 |       1 | Beautiful day in Portland!      | 2018-10-23 07:13:30.664214+00
  2 |       2 | The Avengers movie was so cool! | 2018-10-23 07:13:36.118051+00
  3 |       2 | Sun shine is beautiful          | 2018-10-23 07:13:38.502164+00

# 退出
$ DATABASE=> \q
```

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/43cd0173a5c698b191e42e2c4b244645be01ddd7)

## 部署 Heroku

### 设置Config Vars

可以访问 <https://dashboard.heroku.com/apps/go-mega/settings> dashboard 的Config Vars 进行设置

也可以通过heroku cli

```
$ heroku config:set DBTYPE=heroku

# 设置root url
$ heroku config:set SERVER_URL=https://go-mega.herokuapp.com

# 设置mail
$ heroku config:set MAIL_SMTP=smtp.zoho.com
$ heroku config:set MAIL_SMTP_PORT=587
$ heroku config:set MAIL_USER=your_username
$ heroku config:set MAIL_PASSWORD=your_password

# 查看config
$ heroku config
```

### Procfile

Procfile

```
web: go-mega-code
```

现在我们可以通过 `heroku local`来在本地查看应用

```
$ heroku local
```

### Go dep

目前我们的代码和配置都已经完成了，不过部署 heroku 还需要我们提供依赖，我们这里使用 `Godep`

可以参照[Go Dependencies via Godep](https://devcenter.heroku.com/articles/go-dependencies-via-godep)

```
$ go get -u github.com/tools/godep
$ godep save ./...

# 结果可以看见多了两个文件夹: vendor/ 和 Godeps/
```

### Push heroku

```
# Remote add heroku
$ heroku git:remote -a go-mega
set git remote heroku to https://git.heroku.com/go-mega.git
$ git remote -v
heroku    https://git.heroku.com/go-mega.git (fetch)
heroku    https://git.heroku.com/go-mega.git (push)
origin    git@github.com:bonfy/go-mega-code.git (fetch)
origin    git@github.com:bonfy/go-mega-code.git (push)

# push branch to heroku master
$ git push heroku 14-Deployment-On-Heroku:master
```

现在访问 <https://go-mega.herokuapp.com/> 就能看见 Demo 了

> Notice: Heroku 绑定域名要收费，所以这里就不绑定域名了

![14-01](/files/-LPVuBT4yDg4GRX412uZ)

> 本小节 [Diff](https://github.com/bonfy/go-mega-code/commit/6dd19e4cd7e692e28bc1a091318b1d2a8a6126f7)

## Links

* [目录](/)
* 上一节: [13-Javascript-Magic](/13-javascript-magic)
* 下一节: [15-Deployment-On-Linux](/15-deployment-on-linux)


# 15-Deployment-On-Linux

在本章，我将会把应用部署到Linux服务器, 与上一章相比，这种是比较传统的方式

*本章的GitHub链接为：* [Source](https://github.com/bonfy/go-mega-code/tree/15-Deployment-On-Linux), [Diff](https://github.com/bonfy/go-mega-code/compare/14-Deployment-On-Heroku...15-Deployment-On-Linux), [Zip](https://github.com/bonfy/go-mega-code/archive/v1.5.zip)

## Vultr

当提到“传统托管”时，意思是应用是手动或通过原始服务器机器上的脚本安装部署的。 该过程涉及安装应用程序、其依赖项和生产规模的Web服务器，并配置系统以确保其安全。

当你要部署自己的项目时，要问的第一个问题是在哪找服务器。 目前有很多经济的托管服务。 只需每月2.5 - 5美元，[Vultr](https://www.vultr.com/),[Digital Ocean](https://www.digitalocean.com/)和[Linode](https://www.linode.com/)

这里我选择的是 Vultr 创建了 CentOS 7 的linux 服务器

> 广告: 这里大家可以通过这个地址注册 Vultr，我会得到 10 dollar 的奖励，如果想搞个VPS自己玩玩的同学可以考虑下，还能搭个那啥用(自己体会) 链接: <https://www.vultr.com/?ref=7079936>

## ssh 免密登陆

```
$ ssh-keygen -t rsa -C  'your email'

$ scp ~/.ssh/id_rsa.pub username@hostname:~/ # 将公钥文件复制至ssh服务器
$ ssh username@hostname # 使用用户名和密码方式登录至ssh服务器
$ mkdir .ssh  # 若.ssh目录已存在，可省略此步
$ cat id_rsa.pub >> .ssh/authorized_keys  # 将公钥文件id_rsa.pub文件内容追加到authorized_keys文件
```

以后登录ssh 只要输入 `ssh username@hostname` 即可不用输入密码啦

## 安装mysql

```
$ ssh username@hostname

# Install
$ yum -y install mariadb mariadb-serve

# Start mariadb
$ sudo systemctl start mariadb
$ sudo systemctl enable mariadb

# Securing the MariaDB Server
# Set Password
$ sudo mysql_secure_installation

# crate database
$ mysql -uroot -p
$ MariaDB > create database go-mega;
```

## 部署应用

与 Python 相比，Go 部署应用还是算比较方便的，因为它可以编译

### Makefile

为了方便部署，我们这边建一个 `Makefile` 方便我们部署

Makefile

```
.PHONY: clean upload-vps

# Put your server ip or nickname here
host = ny

clean:
    @echo "clean..."
    @rm -rf app/
    @echo "success clean"
build-db:
    @echo "run build db..."
    @env GOOS=linux go build -o app/db cmd/db_init/main.go
    @echo "success build db"
build-server:
    @echo "begin build server..."    
    @env GOOS=linux go build -o app/server main.go
    @echo "success build server"
upload-vps: clean build-db build-server
    @echo "begin run vps upload..."
    @echo "Host: $(host)"
    @scp -r app root@$(host):go-mega
    @scp config.yml root@$(host):go-mega/
    @scp -r templates root@$(host):go-mega/templates
    @echo "success vps upload"
```

注意 上面的 Host 修改成 你自己的 Host ip address 或者 Host 别名

然后我们就比较方便了运行

```
$ make upload-vps
```

就可以了，我们可以登录到我们的vps上查看，就多了一个 go-mega 的文件夹了

这里简要做个说明

* clean: 清楚上次的编译二进制文件
* build-db: 编译 cmd/db\_init/main.go -> app/db
* build-server:  编译 main.go -> app/server
* upload-vps: 将 /app 和 templates 文件夹上传到 vps的 \~/go-mega 里

### 运行

```
$ ssh ssh username@hostname

$ cd go-mega

# 设置环境变量 以及 config.yml 
$ vi config.yml # 按照config.yml.sample 设置
$ export PORT=8000

# 初始化数据库
$ ./db
# 启动应用
$ nohup ./server &

# 这个时候服务已经启动了，这个时候如果想要访问应用可以使用 nginx反代到 80端口 或者 防火墙开放你设置的端口
# 最好是使用nginx，不过这里为了演示，我们开放下端口
$ firewall-cmd --zone=public --add-port=8000/tcp
```

这个时候我们访问 `http://xxx.xxx.xxx.xxx:8000` 就可以访问你刚才发布的应用了, 大功告成

> 这里暂时不是HTTPS的，如果设置HTTPS，需要你自己申请HTTPS证书和配置nginx，这方面比 Heroku 还是麻烦点

## Links

* [目录](/)
* 上一节: [14-Deployment-On-Heroku](/14-deployment-on-heroku)
* 下一节: [16](/16-summary)


# 16-Summary

前两章部署之后，我们的程序其实就能顺利在服务器上运行，接受用户使用了

我们也终于来到了本系列的最后一章，让我们来总结一下

## 知识点

本系列教程使用 Go 语言从无到有实现一个类似初级版 Twitter 的 web网站，主要涉及到了以下的知识点:

* 表单
* 模板
* 数据库
* 路由
* 登陆验证
* MVC
* 分页
* Mail
* 部署
* Ajax
* Jwt

与原教程 Flask-Mega 相比，我们主要少了 `全文搜索`、`Docker`、`用户通知` 这几个章节

* 其中 Docker 对于 Go 程序来说还是很方便的，因为 Go 可以直接编译成二进制程序，所以一种写 Dockerfile 的思路就是先编译 然后 COPY 进 container 就行了
* 全文搜索 其实是对 Elasticsearch 的依赖，其实 Go 也有响应的库支持，这里不引入只是一开始就只打算加一个数据库，不想涉及更多的依赖，增加程序的复杂性
* 用户通知其实是对 Ajax 和 API 的更深入的一种实现，其实有了前面的知识，还是很容易能完成这个功能，如果说有点难度，只是 Go 的Template 没有Flask 那么灵活而已

## 总结

这是我第一次系统性地完成编写一套系列教程

由于有个Flask-Mega的模板在，在章节设计方面已经节省了好多时间，不过 代码 和 文字方面还是要花费不少的精力，基本上3天一章已经是极限（毕竟还是要上班养家糊口的）

通过编写这个系列教程，也算是我自己重新梳理了下知识结构，相信有这个教程在，也避免了我写完就忘的神奇特性（以前也写过好多程序，过段时间回首，知识剩下的就不多了）

好记性不如烂笔头，写下来方便自己，如果还能方便他人，那真的是再好不过了。

## 后续

后续首要第一件事情就是休息，先休息一段时间

然后在下面几方面好好整理，写点东西

* Test
* Restful API
* GraphQL

## 支持我

如果您感觉有收获，欢迎给我打赏，以激励我输出更多优质内容，感谢支持！

![感谢支持](/files/-LNIhG0Z3FreAiCXBFsn)


