2013年2月

本文原文为How to write database-driven Web application using Go的 README.md 文件,如果您想查看本文的原文,请点击前面的英文原文标题,找到该项目的 README.md 文件即可,格式为 *MarkDown*,如果你需要HTML版本的,可能还需要自己安装MarkDown相关的工具,本示例的 Github 地址为:https://github.com/pantao/example-go-wiki/


本文将尝试前着去介绍如何使用 kview/kasia.go 以及 MyMySQL 基于Go语言开发一个小型的简单的数据库驱动的Web应用,就像大家做的一样,我们来开发一个小型的维基系统。

对您个人的要求

  • 一些程序开发经验;
  • 关于 HTML 与 HTTP 的基础知识;
  • 了解MySQL 以及 MySQL 命令行工具的使用;
  • 在MySQL中创建数据库;
  • 最新的 Go 编译器 - 移步 Go 首页 以了解更多详情。

数据库

让我们从创建应用所使用的数据库开始该项目。

如果你还没有安装MySQL,那么需要你首先安装它,接下来我们会用到它,如果你已经安装了,那么首先我们先来创建该应用数据库。

cox@CoxStation:~$ mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 120
Server version: 5.5.28-0ubuntu0.12.04.2 (Ubuntu)

Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.

mysql> create database go_wiki;
Query OK, 1 row affected (0.00 sec)

mysql> use go_wiki;
Database changed
mysql> CREATE TABLE articles (
    ->     id INT AUTO_INCREMENT PRIMARY KEY,
    ->     title VARCHAR(80) NOT NULL,
    ->     body TEXT NOT NULL
    -> ) DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.09 sec)

mysql> exit
Bye

到现在为止你已经创建了一个可能使用的数据库,同时还添加了一张表 articles*用来保存我们的维基中的文章数据,你可以在应用中直接使用 *root 帐户连接数据库,当然,更好的办法是创建一个独立的用来测试我们应用用户:

mysql> GRANT SELECT, INSERT, UPDATE, DELETE ON articles TO go_wiki@localhost;
Query OK, 0 rows affected (0.00 sec)

mysql> SET PASSWORD FOR go_wiki@localhost = PASSWORD('go_wiki');
Query OK, 0 rows affected (0.00 sec)

现在我们记下刚才所获得的数据:

  • 数据库地址:localhost
  • 数据库名称:go_wiki
  • 数据库用户:go_wiki
  • 数据库密码:go_wiki

视图

现在我们开始写点儿 Go 代码了,像以前一样,创建一个工作空间(如果你还不知道如何创建工作空间,请阅读我以前的文章《如何写 Go 程序》),我在这里就简单的将创建的流程所运行的命令复制到这里:

cox@CoxStation:~$ mkdir go_wiki
cox@CoxStation:~$ cd go_wiki
cox@CoxStation:~/go_wiki$ export GOPATH=$HOME/go_wiki
cox@CoxStation:~/go_wiki$ mkdir bin src pkg
cox@CoxStation:~/go_wiki$ export PATH=$PATH:$HOME/go_wiki/bin

我所创建的工作空间是:

  • HOME: /home/cox
  • GOPATH: $HOME/go_wiki

定义应用的视图我使用 kview 以及 kasia.go 这两个包,你应该先安装它们:

cox@CoxStation:~/go_wiki$ go get github.com/ziutek/kview

上面的命令会因你的网络不同而需要不同的时间,因为Go需要从网络上下载你所需要的一切东西,所以,请保证你的计算机是已经连网了的,下载完成之后,它会自动的为你安装 kasia.go 和 *kview*。

下一步,在 $GOPATH/src 中创建我们的项目:

在 $GOPATH/src/go_wiki*目录中,创建 *view.go 文件,它的内容如下:

/*
A simple wiki engine based on mysql database and golang.
*/
package main

// 导入 kview
import "github.com/ziutek/kview"

// 声明我们的维基页面视图
var main_view, edit_view kview.View

func init() {
    // 加载 layout 模板
    layout := kview.New("layout.kt")

    // 加载用来展示文章列表的模板
    article_list := kview.New("list.kt")

    // 创建主页面
    main_view = layout.Copy()
    main_view.Div("left", article_list)
    main_view.Div("right", kview.New("show.kt"))

    // 创建编辑页面
    edit_view = layout.Copy()
    edit_view.Div("left", article_list)
    edit_view.Div("right", kview.New("edit.kt"))
}

如你所看到的,我们的应用将由两人个页面组成:

  • main_view - 用来向用户展示文章内容
  • edit_view - 向用户提供创建与编辑功能

任何一个页面又都有两个子栏组成:

  • left - 已存在的文章列表
  • right - 根据页面不同而展示不同的内容(主页为内容展示,编辑页为编辑表单)

接下来我们来创建第一个 Kasia 模板,它将定义我们网站的整个页面布局,我们需要在模板目录中创建一个 layout.kt 文件:

cox@CoxStation:~/go_wiki/src/go_wiki$ mkdir templates
cox@CoxStation:~/go_wiki/src/go_wiki$ emacs templates/layout.kt

它的内容为:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>MySQL数据库驱动的基于Go的维基系统</title>
<link href="/style.css" type="text/css" rel="stylesheet" />
</head>
<body>
<div>
    <h1>MySQL数据库驱动的基于Go的维基系统</h1>
    <div>
        <div>$left.Render(Left)</div>
        <div>$right.Render(Right)</div>
    </div>
</div>
</body>
</html>

上面这个简单布局的职责是:

  • 创建基础的HTML文档结构,包括 doctype 、 head 、 body 等
  • 渲染 left 与 right 两个分栏的具体内容,使用的数据是由 Left 与 Right 提供的

Render 方法是由 kview 包提供的,它能在特定的调用它的位置根据提供给它的数据渲染出子视图,这样的子视图可以有它自己的布局、div元素等等,并且子视图中还可以其自己的子视图(但是对于我们现在的这个小项目来说是完全没有必要的了)。

下一步,我们创建 list.kt ,它将被渲染到 left 中,它的内容是:

<a href="/edit/">创建新文章</a>
<hr />
<h2>最近更新</h2>
<ul>
$for _, article in Articles:
    <li><a href="$article[Id]">$article[Title]</a></li>
$end
</ul>

这个模板输出一个创建新文章*的链接以及一个文章标题链接列表。它使用到了一个 *for 声明用来遍历 Articles 列表(它是一个分片(slice)),之后的遍历得到的每一个项都使用 article[Id} 来创建与该文章对应的页面的URL地址,以及 article[Title] 来创建标题,Articles*、*Id*、以及*Title*都是 *ArticleList 类型中定义的成员(我们会在本文档的后面来定义它),*Id*与*Title*都将对应到*Row*片段中相应的记录,*article*将直接存储从数据库中取得的记录中的某一条。

for 声明在本地创建两个局部变量(_ 与 *article*),第一个是遍历次数,我们不需要它,所以直接将其丢掉,第二个就是从数据库取得的一条记录,对于那个遍历次数,虽然我们这里没有使用到它,但是它却很有用,比如下面这样的:

$for nn+, article in Articles:
    <li>
        <a href="$article[Id]">$article[Title]</a>
    </li>
$end

这会在每一个记录上面添加一个类,如果是第奇数条记录,则添加 *odd*,否则添加 *even*,我们这里使用 nn+ 是因为我们希望第一条记录在输入时, nn 不会 0,而是1。

现在来创建 show.kt 模板,它将被用来渲染文章数据:

<div>
$if Id:
    <h2>$Title</h2>
    <div>
        $Body
    </div>
    <p>
        <a href="/edit/$Id">编辑本文</a>
    </p>
$else:
    <h2>维其示例页面</h2>
    <div>
        <p>您现在所看到是本维基的示例页面,这说明您现在还没有指定任何内容。</p>
        <p>本维基使用MySQl数据库存储数据,基于Go语言开发。</p>
        <p>点击下方或者左侧的"创建新文章"链接以创建您的第一篇文章。</p>
        <h3>本维其使用到的技术</h3>
        <ul>
            <li><a href='https://github.com/ziutek/kasia.go'>kasia.go</a></li>
            <li><a href='https://github.com/ziutek/kview'>kview</a></li>
            <li><a href='https://github.com/ziutek/mymysql'>MyMySQL</a></li>
        </ul>
    </div>
    <p>
        <a href="/edit/">创建新文章</a>
    </p>
$end
</div>

在这里你可以看到,我们使用了一个 *if-else*声明,如果我们指定了展示哪条文章数据了,那么就展示这些数据,如果没有的话,我们就展示一个默认的内容页(当然,这个内容页是无法让前端用户修改的)。

插一点有半 上下文堆栈 的知识

要使用 kview 包渲染某些模板,你需要用到两人个方法,在 Go 代码中的话,就是 *Exec*,在模板代码中的话,就是 *Render*,通常的你还需要给它们传递一些变量,比如像下面这样的:

v.Exec(wr, a, b)
v.Render(a, b)

与视图 v 关联的模板将以下面这样的上下文堆栈渲染:

[]interface{}{globals, a, b}

你可以看到,这里有一个 globals 变量,它是一个 *map*,包含了一些全局变量:

  • 子视图(或者子模板)通过 Div 方法添加到视图 v 中
  • len 以及 fmt 工具
  • 你传递给 New 函数的变量也会被动态的添加到这些全局变量中

如果你想了解得更详细一些,可以看看 kview文档

变量 b 处于这个堆栈的最底端,如果你在模板中这样写:

$x $@[1].y

那么 Exec 或者 Render 方法将以下面这种方式去搜索 x 或者 y 属性:

  • 首先在 b 中搜索 x*,如果没有找到,再在 *a 中搜索,如果还是没有找到,那么再去公用的全局变量中搜索。
  • y 将只会在 a 中进行搜索,因为你已经直接指定了要在当前堆栈的那一个元素中去搜索(*@[1]y*)。

在上面的示例中,符号 @ 表示堆栈自向,所以,你可以像下面这样的写:

  • Go代码: v.Exec(os.Stdout, “Hello”, “World!”, map[string]string{“cox”: “Antu”})
  • 模板中: $@[1] $@[2] $@[1] $@ $cox!
  • 输出: Hello World! Hello Antu!

最后,你还可以像下面这样输出整个堆栈以查看它的所有内容:

$for i, v in @:
    $i: $v<br />
$end

想了解更多请移步Kasia.go文档

插了这点小知识之后,我们该回到前面一直在进行中的项目了,让我们来创建最后一个模板 edit.kt文件:

<form action="/$Id" method="post">
<h2>
    <input type="text" name="title" id="title" value="$Title" placeholder="标题:$Title"/>
</h2>
<div>
    <textarea name="body" id="body" placehoder="内容:$Body">$Body</textarea>
</div>
<div>
    <input type="submit" value="退出" />
    <input type="submit" name="submit" value="保存" />
</div>
</form>

我们现在需要一个样式表来让这个维基好看一些,当然这不是必须的,你可以直接使用我下面的这一份样式表:

body {
font: 16px/1.62 "Xin Gothic", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", Arial, sans-serif;
color: #333;
margin: 0;
}
h1, h2, h3, h4, h5, h6, strong, em {
font-family: "Xin Gothic", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", Arial, sans-serif;
}
h1 {
margin: 0;
padding: .5em;
background: #ddd;
border-bottom: .1em solid #aaa;
}
h1 a {
text-decoration: none;
color: #333;
}
h2 {
margin: 0;
padding: .5em;
}
h2 input {
border: .1em solid #aaa;
font-size: 1em;
width: 80%;
}
div.field {padding: 1em}
div.field label {
font-size: 1.5em;
display: block;
}
textarea {
width: 90%;
border: .2em solid #aaa;
font-size: 1.2em;
line-height: 1.5em;
height: 10em;
}
.columns {
letter-spacing: -.45em;
}
.columns .left, .columns .right {
display: inline-block;
letter-spacing: normal;
min-height: 20em;
vertical-align: top;
}
.columns .left {
width: 30%;
border-right: .2em solid #aaa;
}
.columns .right {
width: 69%;
border-left: .2em solid #aaa;
}
.content {
padding: .5em 1em;
}
.button {
display: inline-block;
border: .1em solid #ddd;
background: #eee;
border-radius: .4em;
padding: .5em 1em;
margin: .5em;
color: #333;
text-decoration:none;
}
.button:hover {
background: #999;
}
.left a.button {
width: 70%;
text-align: center;
}
.list {
list-style: none;
padding: 0 .8em;
}
.list a {
display: inline-block;
text-decoration: none;
color: #333;
}

连接到 MySQl 数据库服务器

我们使用 MyMySQL 包来连接 MySQl 数据库,先安装它:

cox@CoxStation:~/go_wiki$ go get github.com/ziutek/mymysql/autorc

现在我们可以为我们的应用写 MySQl 连接器了,创建一个 mysql.go 文件,在该文件的第一部分我们会导入一些必须的包,定义一些常量和全局变量:

/*
维基的MySQl操作器
*/
package main

import (
    "os"
    "log"
    "github.com/ziutek/mymysql/mysql"
    "github.com/ziutek/mymysql/autorc"
    _ "github.com/ziutek/mymysql/thrsafe"
)

const (
    db_proto = "tcp"
    db_addr  = "127.0.0.1:3306"
    db_user  = "go_wiki"
    db_pass  = "go_wiki"
    db_name  = "go_wiki"
)

var (
    // MySQl 连接处理器
    db = autorc.New(db_proto, "", db_addr, db_user, db_pass, db_name)

    // 预备声明
    artlist_stmt, article_stmt, update_stmt *autorc.Stmt
)

声明之后,MySQL 连接处理器已经可以连接到数据库了,但是我们并不会明显的去连接它。

在我们的应用中,我们将使用 MyMySQl 的 autorecon 接口,这是一些不需要连接数据库即可使用的函数集,更重要的是,使用它们,我们将不需要在因为网络原因或者数据库服务器重启导致与数据库连接中断之后重新手动再次连接数据库,它们会帮我们做好这些事情。

下一步我们定义一些 MySQL 错误处理程序:

func mysqlError(err error) (ret bool) {
    ret = (err != nil)
    if ret {
        log.Println("MySQL error:", err)
    }
    return
}

func mysqlErrExit(err error) {
    if mysqlError(err) {
        os.Exit(1)
    }
}

接着再来定义初始化函数:

func init() {
    var err error

    // 初始化命令
    db.Raw.Register("SET NAMES utf8")

    // 准备好服务器商的声明

    artlist_stmt, err = db.Prepare("SELECT id, title FROM articles")
    mysqlErrExit(err)

    article_stmt, err = db.Prepare(
        "SELECT title, body FROM articles WHERE id = ?",
    )
    mysqlErrExit(err)

    update_stmt, err = db.Prepare(
        "INSERT articles (id, title, body) VALUES (?, ?, ?)" +
        " ON DUPLICATE KEY UPDATE title=VALUES(title), body=VALUES(body)",
    )
    mysqlErrExit(err)
}

Register 方法所注册的命令将在数据库连接创建之后立马执行, Prepare 方法则准备好服务器端的声明,因为我们使用了 mymysql/autorc 包,所以,当我们在准备第一个服务器端声明时,数据库连接就会被创建。

我们使用 预先声明 来代替普通的数据库查询的原因是这样做更加的安全,现在我们不再需要任何的其它函数来过滤用户输入的数据,因为SQL的逻辑与数据已经被完全分离开了,如果不这样做,我们总是很容易被别人进行数据库注入攻击。

下面让我们来创建从数据库中获取数据提供给页面使用的代码:

type ArticleList struct {
    Id, Title int
    Articles []mysql.Row
}

// 返回文章数据列表给 list.kt 模板使用,我们不使用 map 是因为那需要
// 做太多的事情,你或许在以后的项目中应该这么做,但是在这里,为了简单,
// 我们直接提供原始的 query 结果集以及索引给 id 和 title 字段。
func getArticleList() *ArticleList {
    rows, res, err := artlist_stmt.Exec()
    if mysqlError(err) {
        return nil
    }
    return &ArticleList{
        Id:       res.Map("id"),
        Title:    res.Map("title"),
        Articles: rows,
    }
}

再定义函数用来获取或者更新文章数据:

type Article struct {
    Id int
    Title, Body string
}

// 获取一篇文章
func getArticle(id int) (article *Article) {
    rows, res, err := article_stmt.Exec(id)
    if mysqlError(err) {
        return
    }
    if len(rows) != 0 {
        article = &Article{
            Id:    id,
            Title: rows[0].Str(res.Map("title")),
            Body:  rows[0].Str(res.Map("body")),
        }
    }
    return
}

// 插入或者更新一篇文章,它返回被更新/新插入的文章记录的id
func updateArticle(id int, title, body string) int {
    _, res, err := update_stmt.Exec(id, title, body)
    if mysqlError(err) {
        return 0
    }
    return int(res.InsertId())
}

最后的那个函数使用了 MySQL INSERT … ON DUPLICATE KEY UPDATE 查询,它的功能是 如果ID存在则更新,否则就插入新数据。

控制器

程序的最后一步就是创建一个控制器来与用户进行互动了,让我们创建 controller.go 文件:

package main

import (
    "log"
    "net/http"
    "strconv"
    "strings"
)

type ViewCtx struct {
    Left, Right interface{}
}

// 渲染主页
func show(wr http.ResponseWriter, art_num string) {
    id, _ := strconv.Atoi(art_num)
    main_view.Exec(wr, ViewCtx{getArticleList(), getArticle(id)})
}

// 渲染编辑页面
func edit(wr http.ResponseWriter, art_num string) {
    id, _ := strconv.Atoi(art_num)
    edit_view.Exec(wr, ViewCtx{getArticleList(), getArticle(id)})
}

// 更新数据库以及渲染主页
func update(wr http.ResponseWriter, req *http.Request, art_num string) {
    if req.FormValue("submit") == "保存" {
        id, _ := strconv.Atoi(art_num) // id == 0 表示创建新文章
        id = updateArticle(
            id, req.FormValue("title"), req.FormValue("body"),
        )
        // 如果我们插入一篇瓣文章,我们就修改 art_num 为新插入文章的 id
        // 这使得我们可以在成功插入新数据之后立马展示该条数据
        art_num = strconv.Itoa(id)
    }
    // 重定身至主页面并展示新插入的文章
    http.Redirect(wr, req, "/"+art_num, 303)
    // 我们可以直接使用 show(wr, art_num) 展示新插入的文章
    // 但是请查阅:http://en.wikipedia.org/wiki/Post/Redirect/Get
}

// 根据客户请求的方式以及URL地址来选择使用哪个控制器来处理
func router(wr http.ResponseWriter, req *http.Request) {
    root_path := "/"
    edit_path := "/edit/"

    switch req.Method {
    case "GET":
        switch {
        case req.URL.Path == "/style.css" || req.URL.Path == "/favicon.ico":
            http.ServeFile(wr, req, "static"+req.URL.Path)

        case strings.HasPrefix(req.URL.Path, edit_path):
            edit(wr, req.URL.Path[len(edit_path):])

        case strings.HasPrefix(req.URL.Path, root_path):
            show(wr, req.URL.Path[len(root_path):])
        }

    case "POST":
        switch {
        case strings.HasPrefix(req.URL.Path, root_path):
            update(wr, req, req.URL.Path[len(root_path):])
        }
    }
}

func main() {
    err := http.ListenAndServe(":2223", http.HandlerFunc(router))
    if err != nil {
        log.Fatalln("ListenAndServe:", err)
    }
}

在上面的代码中:

  • show 绑定了 GET 方法以及 /(.)* 这个URL结构,它被用来展示主页视图以及展示用户选择查看的文章。
  • edit 绑定了 GET 方法以及 /edit/(.)* URL 结构,它负责处理渲染编辑页面
  • update 绑定了 POST 方法以及 /(.)* URL结构,它负责更新文章数据到数据库,只有当用户点击了“ 保存 ”按钮才更新数据,如果点击的是 取消 那么直接不进行任何处理,当更新完成之后,将页面重定向至刚才更新的文章的展示页面。

运行我们的应用

cox@CoxStation:~/go_wiki/src/go_wiki$ go run *.go

上面这行命令就可以运行我们的应用了,我们可以创建一个 Bash 脚本来启动我们的应用:

Ccox@CoxStation:~/go_wiki/src/go_wiki$ emacs run.sh

其内容为:

go run *.go

之后为其添加可执行权限,再运行它即可启动我们的应用:

cox@CoxStation:~/go_wiki/src/go_wiki$ chmod +x run.sh
cox@CoxStation:~/go_wiki/src/go_wiki$ ./run.sh

从 controlle.go 代码中可以知道,我们的应用监听的是 2223 端口,打开浏览器,地址栏中输入:http://127.0.0.1:2223 即可访问到我们的应用。:

获取该示例的代码

你可以通过下面的命令获取到该示例的英文原版:

git clone git://github.com/ziutek/simple_go_wiki.git

或者直接将其安装到你当前环境下 $GOPATH 所指定的工作空间中:

go get github.com/ziutek/simple_go_wiki

如果你对我的翻译版本(与原版有些话不同)感兴趣的话,可以使用下面的地址下载:

其它框架

本示例中使用的是 http 完成的 controller,你还可以使用如 web.go 或者 twister 来重新实现。

使用 Markdown 格式化文章内容

我们的示例现在的文章内容是直接从数据库中读取的没有任何格式的纯文本,这在网页中显示出来之后就是一大段连续的没有分段分行的纯文本,很不符合我们的阅读,尤其是像本文这种需要清晰的格式化与条理的文章更加无法阅读了,所以,我们还可以选择一些文本格式化工具,比如我一直使用的 Markdown ,这需要你安装 markdown package 。

要在我们的项目中使用 Markdown,首先安装 Markdown 包:

cox@CoxStation:~/go_wiki$ go get github.com/knieriem/markdown

然后还需要修改两人个文件 view.go 以及 show.kt :

在 view.go 中,做下面这样的修改:

import "github.com/ziutek/kview"

改为:

import (
    "bufio"
    "bytes"
    "github.com/ziutek/kview"
    "github.com/knieriem/markdown"
)

然后添加工具函数:

var (
    mde = markdown.Extensions{
        Smart:        true,
        Dlists:       true,
        FilterHTML:   true,
        FilterStyles: true,
    }
    utils = map[string]interface{} {
        "markdown": func(txt string) []byte {
            p := markdown.NewParser(&mde)
            var buf bytes.Buffer
            w := bufio.NewWriter(&buf)
            r := bytes.NewBufferString(txt)
            p.Markdown(r, markdown.ToHTML(w))
            w.Flush()
            return buf.Bytes()
        },
    }
)

再将它添加到 show.kt 的全局变量中去:

main_view.Div("right", kview.New("show.kt", utils))

最后我们需要在 show.kt 文件中将 $Body 修改为 $:markdown(Body) 即可。

应用运行截图

Golang Wiki 编辑页

Golang Wiki 前端阅读页

模板集是Go中一种特殊的数据类型,它允许你将若干有关联性的模板归为一个组,当然了,它不是一个个体的数据类型,而是被归入了模板的数据结构中。所以,如果你在一些文件中使用了{{define}}…{{end}} 声明,那么这一块就成为了一个模板。

比如一个网页,有头部(Header),主体(Body)以及页脚(Footer),那么这些区域我们都可以在一个文件中定义为一个一个的模板,它们将可以通过一次请求读取到你的程序中,例如:

{{define "header"}}
<html>
<head></head>
{{end}}

{{define "body"}}
<body></body>
{{end}}

{{define "footer"}}
</html>
{{end}}

这里的关键知识是:

  • 每一个模板都是由 {{define}}…{{end}} 对定义的
  • 每一个模板都给定了一个唯一的名称——如果你在一个文件中定义重复使用一个名称将引起一个 Panic
  • 不允许将任何字符写在 {{define}}…{{end}}对外——否则会引起Panic

这上面这样的文件被解析为一个模板集之后,Go自动的创建一个以模板名称为键,模板内容为值的 Map:

tmplVar["header"] = pointer to parsed template of text "<html> … </head>"
tmplVar["body"] = pointer to parsed template of text "<body> … </body>"
tmplVar["footer"] = pointer to parsed template of text "</html>"
同一个模板集中的模板是知道有其它模板存在的,所以如果有一个模板是可以在各个其它模板中调用的,那么你需要将这个模板单独保存为一个文件,然后再在需要的地方调用它。

现在我们来使用一个简单的示例更好的说明这一切:

  • 定义一个模板——使用 {{define}}…{{end}}
  • 在一个模板中引入另一个模板——使用 {{template “template name”“}}
  • 解析多个文件成为一个模板集——使用 template.ParseFiles
  • 执行或者转换模板——使用 template.ExecuteTemplate

先创建两人个模板文件:

  1. 模板文件一:t1.tmpl
    {{define "t_ab"}}a b{{template "t_cd"}}e f {{end}}

    上面这个文件将被解析为名为 t_ab 的模板,它还引入了一个名为 t_cd 的模板
  2. 模板文件二:t2.tmpl
    {{define "t_cd"}} c d {{end}}

    上面这个文件将被解析为 t_cd 模板

程序代码:

package main

import (
    "text/template"
    "os"
    "fmt"
)

func main() {
    fmt.Println("Load a set of templates with {{define}} clauses and execute:")
    s1, _ := template.ParseFiles("t1.tmpl", "t2.tmpl") //create a set of templates from many files.
    //Note that t1.tmpl is the file with contents "{{define "t_ab"}}a b{{template "t_cd"}}e f {{end}}"
    //Note that t2.tmpl is the file with contents "{{define "t_cd"}} c d {{end}}"

    s1.ExecuteTemplate(os.Stdout, "t_cd", nil) //just printing of c d
    fmt.Println()
    s1.ExecuteTemplate(os.Stdout, "t_ab", nil) //execute t_ab which will include t_cd
    fmt.Println()
    s1.Execute(os.Stdout, nil) //since templates in this data structure are named, there is no default template and so it prints nothing
}

输出结果为:

c d
a b c d e f

template.ParseGlob 与 template.ParseFiles 类似,只是它只需要接收一个通配符*作为参数,比如上面的这个示例,你可以使用 *template.ParseGlob("t.tmpl”)*也可以得到同样的结果。

http://golang.org/pkg/text/template/ 与 http://golang.org/pkg/html/template/ 是Go官方的关于 *template*包的文档,——html子包提供了一些附加的安装相关的功能,比如防止代码注入的功能经常在我们的网络编程中被使用。

管道(Pipeline)

Unix 用户肯定是明白什么是 Piping 数据,很多程序都提供字符输出——一个字条流,比如你在终端中键入 ls 命令,你将得到一个包含所有当前目录文件名称的列表,它可以被翻译为:“获取当前目录中所有文件名称的列表,然后通过管道将其传送到标准输出,而当前标准输出为命令行终端的屏幕”。

在Unix命令行终端中,管道“*|*”就可以将你一个命令的输入传送给另一个命令,比如:

cox@Cox:~$ ls | grep "a"
access_log
examples.desktop
goworkspace
VirtualBox VMs
workspace

上面的命令 ls 首先获取当前目录下所有文件的列表,然后再通过管道传递给 grep 命令,grep 从里面搜索包含字符“a”的条目,然后再将过滤后的结果通过管道传递给标准输出,标准输出打印出所有结果,如果你还想再过滤,还可以这样:

cox@Cox:~$ ls | grep "a" | grep "V"
VirtualBox VMs

在Go中,我们也可以将任何一个像上面这样的字符流称之为管道,它们同样可以通过管道传送给其它命令,在下面的示例中,处于 {{ }} 中间的字符串与外面的字符串是不一样的——这些静态文本被无修改的复制到外面。

package main

import (
    "text/template"
    "os"
)

func main() {
    t := template.New("Template Test")
    t = template.Must(t.Parse("This is just static text. n{{"This is pipeline data - because it is evaluated within the double braces."}} {{ `So is this, but within reverse quotes.`}}n"))
    t.Execute(os.Stdout, nil)
}

输出为:

This is just static text.
This is pipeline data - because it is evaluated within the double braces. So is this, but within reverse quotes.

模板中的 if-else-end

在模板中使用 if-else 控制结构与在普通代码中使用差不多,在Go模板中,如果传送数据到if的管道为空,则等同为 false:

package main

import (
    "os"
    "text/template"
)

func main() {
    tEmpty := template.New("template test")
    tEmpty = template.Must(tEmpty.Parse("Empty pipeline if demo: {{if ``}} Will not print. {{ end}}n"))
    tEmpty.Execute(os.Stdout, nil)

    tWithValue := template.New("template test")
    tWithValue = template.Must(tWithValue.Parse("Non empty pipline if demo: {{if `anything`}}  Print If Part.{{ end}}n"))
    tWithValue.Execute(os.Stdout, nil)

    tIfElse := template.New("template test")
    tIfElse = template.Must(tIfElse.Parse("If-else demo:{{if `anything`}} Print IF Part. {{ else }} Else part. {{end}}n"))
    tIfElse.Execute(os.Stdout, nil)
}

输出为:

Empty pipeline if demo:
Non empty pipline if demo:   Print If Part.
If-else demo: Print IF Part.

点(*.*)

在Go模板中,点(.)被用来引用当前的管道,你可以想像成为这个点是 Java 中的 this 或者我们在Python中最常使用的 *self*,你可以使用 *{{.}}*引用到它的值。

with-end 结构

with 声明用来设置 点(.) 在管道中的数据:

package main

import (
    "os"
    "text/template"
    )

func main() {
    t, _ := template.New("test").Parse("{{with `hello`}}{{.}}{{end}}!n")
    t.Execute(os.Stdout, nil)

    t1, _ := template.New("test").Parse("{{with `hello`}}{{.}} {{with `Mary`}}{{.}}{{end}}{{end}}!n") //when nested, the dot takes the value according to closest scope.
    t1.Execute(os.Stdout, nil)
}

输出为:

hello!
hello Mary!

模板变量

你可以在模板管道中使用*$*前缀创建本地变量,变量名称必须为英文字母、数字以及下划线:

package main

import (
    "os"
    "text/template"
)

func main() {
    t := template.Must(template.New("name").Parse("{{with $3 := `hello`}}{{$3}}{{end}}!n"))
    t.Execute(os.Stdout, nil)

    t1 := template.Must(template.New("name1").Parse("{{with $x3 := `hola`}}{{$x3}}{{end}}!n"))
    t1.Execute(os.Stdout, nil)

    t2 := template.Must(template.New("name2").Parse("{{with $x_1 := `hey`}}{{$x_1}} {{.}} {{$x_1}}{{end}}!n"))
    t2.Execute(os.Stdout, nil)
}

被重新定义的函数

Go模板同样还提供了一些被重新定义了的函数,比如下面这个示例的 printf 就与 fmt.Sprintf 类似:

package main

import (
    "os"
    "text/template"
)

func main() {
    t := template.New("test")
    t = template.Must(t.Parse("{{with $x := `hello`}}{{printf `%s %s` $x `Mary`}}{{end}}!n"))
    t.Execute(os.Stdout, nil)
}

输出为:

hello Mary!

网络服务总是以HTML网页或者数据的形式回复客户的请求,这通常都带有很多的内容,这包括用户所请求的数据或者HTML结构,使用模板能让我们更方便的将普通的纯文本内容转换为有着特殊格式的文本,比如下面这种情况:

Template:
+---------------+
| Hello, {NAME} |-----+
+---------------+     |     +-------------+
Data:                 +---> + Hello, Mary |
+---------------+     |     +-------------+
| NAME = "Mary" |-----+
+---------------+

在Go中,我们使用 *templates*包中的如 *Parse*、*ParseFile*、*Execute*等方法从一段字符串或者一个模板文件加载模板然后转换为输出,这些用来添加到模板中的数据一些都保存在某个可以被导入的类型字段中,比如上面这个示例可以是这样的实现:

+--------------------+
|template:           |
|"Hello, {NAME}"     |-----+
+--------------------+      -------+     +-------------+
                 template.Execute() +---> + Hello, Mary |
+--------------------+      /-------+     +-------------+
|type struct:        |-----+
|Person{NAME:"Mary"} |
+--------------------+

最典型的使用方法是为网页生成HTML代码的文件,我们需要选择打开一个已经定义好报模板文件,然后使用*template.Execute*方法将一些数据填充到模板文件中,接着使用 *http.ResponseWriter*将结果发入到返回给客户端的数据流。

func handler(w http.ResponseWriter, r *http.Request) {
    t := template.New("some template") // 创建一个新的 template
    t, _ = t.ParseFiles("tmpl/welcome.html", nil) // 从文本文件打开并分析一个模板
    user := GetCurrentlyLoggedInUser() // 一个用来获取当前登陆用户信息的文法
    t.Execute(w, User)
}
  • 对于你真实的网站开始,你可能需要使用 *template.ParseFiles*,但是在这里,我们做为示例,就不再单独写一个模板文件了,而是直接使用 *template.Parse*,它与前者有一样的功能,只是它不是从一个文件中读取模板,而是直接从一个字符串中创建模板。
  • 我们不将示例写作一个Web服务,而是直接将结果打开到我们的命令行终端中。这便得我们需要使用 *io.Stdout*,它关联到我们的标准输出——os.Stdout实现了io.Writer接口。

字段替换——{{.FieldName}}

要在模板文件中输出字段一数据,只需要在该字段名称前面加一个小数点“.*”,然后再将其放入一个双层的大括号“{{}}”里面即可,比如我们需要输出的字段为 *Name*,那么就只需要把 *{{.Name}} 放在模板文件的合适的位置即可。

package main

import (
    "os"
    "text/template"
)

type Person struct {
    Name string // 可导出的字段
}

func main() {
    t := template.New("Hello Template") // 创建一个名为 Hello Template 的模板
    t, _ = t.Parse("Hello {{.Name}}") // 分析模板并创建一个模板
    p := Person{Name:"Mary"} // 定义一个实例
    t.Execute(os.Stdout, p) // 转换模板t,填充了p中的数据
}

输出为:

Hello Mary

为了完整演示,我在这模板里面添加一个不存在的字段nonExportedAgeField*,它是以小写字母开头的,所以未导出,这会导致一个错误,你可以从 *Execute 方法的返回结果中获取到错误信息。

package main

import (
    "os"
    "text/template"
    "fmt"
)

type Person struct {
    Name string
    nonExportedAgeField string
}

func main() {
    p := Person{Name: "Mary", nonExportedAgeField: "44"}
    t := template.New("nonexported template demo")
    t, _ = t.Parse("Hello {{.Name}}! Age is {{.nonExportedAgeField}}.")
    err := t.Execute(os.Stdout, p)
    if err != nil {
        fmt.Println("There was an error:", err)
    }
}

输出为:

Hello Mary! Age is There was an error: template: nonexported template demo:1: can't evaluate field nonExportedAgeField in type main.Person

template.Must函数——检测模板是否正确

template中的静态函数 *Must*可以检测模板是否正确,比如标签是否都已经关闭或者变量是否都是可输出的等等:

package main

import (
    "text/template"
    "fmt"
)

func main() {
    tOk := template.New("first")
    template.Must(tOk.Parse(" some static text /* and a comment */")) //a valid template, so no panic with Must
    fmt.Println("The first one parsed OK.")

    template.Must(template.New("second").Parse("some static text {{ .Name }}"))
    fmt.Println("The second one parsed OK.")

    fmt.Println("The next one ought to fail.")
    tErr := template.New("check parse error with Must")
    template.Must(tErr.Parse(" some static text {{ .Name }")) // due to unmatched brace, there should be a panic here
}

输出为:

The first one parsed OK.
The second one parsed OK.
The next one ought to fail.
panic: template: check parse error with Must:1: unexpected "}" in command

goroutine 1 [running]:
text/template.Must(0x0, 0xf840031030, 0xf840034380, 0x0, 0xf840031030, ...)
    /usr/lib/go/src/pkg/text/template/helper.go:23 +0x4e
main.main()
    /home/cox/workspace/go/src/gotutorial/templates3.go:18 +0x39d

goroutine 2 [syscall]:
created by runtime.main
    /build/buildd/golang-1/src/pkg/runtime/proc.c:221
exit status 2

如果你已经对我以前的辅导教程都有过学习或者有这样的知识,那么你应该可以开始基于Go语言的Web编程了。

网络服务总是在等待用户电脑的连接、请求,并根据用户的请求进行一些处理之后返回用户需要的数据给用户,用户通过URL地址访问网络服务,每一个URL都是唯一的,它代表着网络上的某一个资源,比如你现在看到本文,它的URL地址就对应着这篇文章,不会是别的。

我们的Go语言Web编程的Hello World程序,将运行在本地电脑上,并且会监听9999端口,也就是说,当你打开浏览器,在地址栏里面输入 http://localhost:9999 的时候,就可以访问到我们的这个程序。

package main

import (
    "net/http"
    "fmt"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Inside hanler")
    fmt.Fprintf(w, "Hello World from my Go Program!")
}

func main() {
    http.HandleFunc("/", handler) // 生定向所有URL到 handler 函数
    http.ListenAndServe("localhost:9999", nil) // 监听来自9999端口的连接请求
}

运行它之后,程序将不会有任何显示,但是你可以打开浏览器,在地址栏中输入http://localhost:9999,可以在网页中看到“Hello World from my Go Program!”,这表示你的Web程序已经开始服务了,而这个时候你返回去看运行它的终端,每一次对网页的访问,都会打印出几个“Inside hanler”字符。

现在让我们来分析一下下我们在代码都具体做了什么:

  • 像任何其它的Go程序一样,要想让程序可以执行,那么必须有一个 main 函数
  • 为了在终端打印一些信息,所以我们像其它的示例一样,导入了fmt 包
  • 我们导入了与Web http协议相关的包 http*,所有与该包相关的函数我们都以 *http.FuncName的形式调用。
  • 在 main 函数中,我们将所有的请求都转发给 handler 函数,接着我们呼叫 http.HandleFunc 方法,并且传递了两人个参数,第一个参数为请求的URL地址,第二个为处理该URL请求的函数。
  • 我们使用 http.ListenAndServer() 方法打开服务程序,并且监听了 localhost:9999
  • 当我们打开浏览器访问 *http://localhost:9999*的时候,程序返回了“Hello World from my Go Program!”字符串。
  • 从 http.Request 你可以获取关于当次请求的所有细节,比如URL地址,或者用户输入的数据等。
  • 你应该使用接收到的 http.ResponseWriter 指针来发送需要返回给客户的内容。
  • 代码 *http.HandleFunc(“/”, handler) 表示所有针对该地址的请求都将转发给 *handler*函数

现在让我们对上面的示例程序稍稍地扩展一下,让它可以接收一些用户的数据,然后将用户输入的数据展示给用户:

package main

import (
    "net/http"
    "fmt"
    "strings"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    remPartOfURL := r.URL.Path[len("/hello/"):] //获取 URL 地址中 /hello/ 部分之后的所有内容
    fmt.Fprintf(w, "Hello %s", remPartOfURL)
}

func shouthelloHandler(w http.ResponseWriter, r *http.Request) {
    remPartOfURL := r.URL.Path[len("/shouthello/"):] // 同上
    fmt.Fprintf(w, "Hello %s!", strings.ToUpper(remPartOfURL))
}

func main() {
    http.HandleFunc("/hello/", helloHandler)
    http.HandleFunc("/shouthello/", shouthelloHandler)
    http.ListenAndServe("localhost:9999", nil)
}

现在,运行该程序,然后访问下面这些地址看看:

本文是Go 中的 Channels的下半部分,如果你还没有阅读过第一部分,建议你先阅读它。

这里我们还有一个问题,那就是数据的接收者无法知道什么时候应该停止等待数据,是否还有更多新数据或者所有数据都已经接收到了?我们应该继续等待还是可以进行下一步的工作了?一种解决办法是不断的循环查询数据来源是否已经关闭,如果Channel已经关闭,则知道数据已经全部接收完成了,但是这并不十分有效。Go提供了一个关键字 *range*,它可以帮助我们一直监听Channel直到它关闭。

package main

import (
    "fmt"
    "time"
    "strconv"
)

func makeCakeAndSend(cs chan string, count int) {
    for i := 1; i <= count; i++ {
        cakeName := "Strawberry Cake " + strconv.Itoa(i)
        cs <- cakeName // 传递一个 cake
    }
}

func receiveCakeAndPack(cs chan string) {
    for s := range cs {
        fmt.Println("Packing received cake: ", s)
    }
}

func main() {
    cs := make(chan string)
    go makeCakeAndSend(cs, 5)
    go receiveCakeAndPack(cs)

    du,_ := time.ParseDuration("3s")
    time.Sleep(du)
}

输出为:

Packing received cake:  Strawberry Cake 1
Packing received cake:  Strawberry Cake 2
Packing received cake:  Strawberry Cake 3
Packing received cake:  Strawberry Cake 4
Packing received cake:  Strawberry Cake 5

对于 makeCakeAndSend*来说,我们已经指定了要制作的蛋糕数,但是对于 *receiveCakeAndPack*来说,它事先并不知道将有多少个蛋糕需要它打包,在上一个示例中,我们将这个蛋糕数硬编码至代码中,所以,它知道什么时候完成,但是现在我们改用了 *range 这个关键字,现在当 channel 关闭之后,循环会自动退出。

Channel 与 select

我们还有一种情况,那就是多台生产蛋糕的电脑可以通过多个传送带向同一个打包的电脑传送蛋糕,因为理论上讲蛋糕生产速度是没有打包速度快的,所以,我现在两人台电脑同时生产蛋糕,然后配备一台电脑用来打包,这个时候我们可以使用一个传送带(一个Channel):

package main

import (
    "fmt"
    "time"
    "strconv"
)

func makeCakeAndSend(cs chan string, flavor string, count int) {
    for i := 1; i <= count; i++ {
        cakeName := flavor + " Cake " + strconv.Itoa(i)
        cs <- cakeName // 传递一个 cake
    }
}

func receiveCakeAndPack(cs chan string) {
    for s := range cs {
        fmt.Println("Packing received cake: ", s)
    }
}

func main() {
    cs := make(chan string)
    go makeCakeAndSend(cs, "Strawberry", 5)
    go makeCakeAndSend(cs, "Chocolate", 5)
    go receiveCakeAndPack(cs)

    du,_ := time.ParseDuration("2s")
    time.Sleep(du)
}

运行结果为:

Packing received cake:  Strawberry Cake 1
Packing received cake:  Chocolate Cake 1
Packing received cake:  Strawberry Cake 2
Packing received cake:  Chocolate Cake 2
Packing received cake:  Strawberry Cake 3
Packing received cake:  Chocolate Cake 3
Packing received cake:  Strawberry Cake 4
Packing received cake:  Chocolate Cake 4
Packing received cake:  Strawberry Cake 5
Packing received cake:  Chocolate Cake 5

但是我们还可以使用一个 select 关键字来以另一种方式选择,我们为了提供生产效率,除了采用两人台电脑生产蛋糕之外,我们还使用两个传送带来传送,这样就不会因为传送带一次只能传递一个蛋糕而影响效率了:

package main

import (
    "fmt"
    "time"
    "strconv"
)

func makeCakeAndSend(cs chan string, flavor string, count int) {
    for i := 1; i <= count; i++ {
        cakeName := flavor + " Cake " + strconv.Itoa(i)
        cs <- cakeName
    }
}

func receiveCakeAndPack(strbry_cs chan string, choco_cs chan string) {
    strbry_closed, choco_closed := false, false
    for {
        if (strbry_closed && choco_closed) { return }
        fmt.Println("Waiting for a new cake ...")
        select {
        case cakeName, strbry_ok := <- strbry_cs:
            if (!strbry_ok) {
                strbry_closed = true
                fmt.Println("... Strawberry channel closed!")
            } else {
                fmt.Println("Received from Strawberry channel. Now packing ", cakeName)
            }
        case cakeName, choco_ok := <- choco_cs:
            if (!choco_ok) {
                choco_closed = true
                fmt.Println("... Chocolate channel closed!")
            } else {
                fmt.Println("Received from Chocolate channel. Now packing ", cakeName)
            }
        }
    }
}

func main() {
    scs := make(chan string)
    ccs := make(chan string)
    go makeCakeAndSend(scs, "Strawberry", 3)
    go makeCakeAndSend(ccs, "Chocolate", 3)
    go receiveCakeAndPack(scs,ccs)
    du, _ := time.ParseDuration("3s")
    time.Sleep(du)
}

输出结果为:

Waiting for a new cake ...
Received from Chocolate channel. Now packing  Chocolate Cake 1
Waiting for a new cake ...
Received from Chocolate channel. Now packing  Chocolate Cake 2
Waiting for a new cake ...
Received from Strawberry channel. Now packing  Strawberry Cake 1
Waiting for a new cake ...
Received from Chocolate channel. Now packing  Chocolate Cake 3
Waiting for a new cake ...
Received from Strawberry channel. Now packing  Strawberry Cake 2
Waiting for a new cake ...
Received from Strawberry channel. Now packing  Strawberry Cake 3
Waiting for a new cake ...

Goroutines 允许你以并行的方式运行程序,但是要让这种并行有实际的用途,我们还有一些其它的需求——我们应该能够向运行中的某一个进程传递数据,并且也应该可以从一个运行中的进程中获取数据,这就需要使用到Go的Channels了。

一个Channel可以被想象成为一个定义了大小和能力的管道或传送带,我们可以将数据从它的一侧放入,然后在另一侧取出。我们拿蛋糕工厂中的传送带来打比方的话,Channel就是那个传送带,有一台电脑负责制作蛋糕,它将做好的蛋糕放到传送带上面来,然后另一台负责打包的电脑就在等待传送带上面的蛋糕,当它等到一个制作好的蛋糕之后就将其从传送带上面取出然后打包进行下一个处理,在这个过程中,传送带(也就是我们的Channel)起到了让负责制作和打包的两台电脑通讯的功能。

传送带就可以被称之为Channel
+++++++++++++++++++++++++++++++++ +   打好包的蛋
制作电脑 -> -> 蛋糕 -> -> 蛋糕 -> -> 蛋糕 -> 打包电脑 -> ->  ++ 糕,可以进
+++++++++++++++++++++++++++++++++ +   行其它处理

在Go中,关键字 chan 被用来定义Channel,make 关键字则被用来创建它,并且定义它能传送的数据类型(是传送蛋糕的传送带而不是传送鸡蛋的)。

ic := make(chan int)
sc := make(chan string)
mc := make(chan mytype)

你可以使用 <- 操作符来向Channel传送或者取回数据,比如:

ic := make(chan int)
//在某些Goroutines中向其传送数据:
ic <- 5
//在另一些Goroutines中取出其中的数据
var recv int
recv = <- ic

你还可以使用 <- 操作符定义Channel中数据的移动方向:

sendOnlyChan := make( <- chan int) //只能发送数据
recvOnlyChan := make( chan -< int) //只能接收数据

一个Channel可以容纳数据的容量是十分分重要,它表示了多少个项目可以被同时存储在Channel中,就比如制作蛋糕的电脑一分钟可以制作十个蛋糕,但是打包的电脑却只能一分钟打包五个,这样就会有太多的蛋糕因为无法来得急打包而制作电脑又源源不断的传送带上面放新制作好的蛋糕而被丢掉浪费,这在计算机并行运算中,就是所谓的生产者-消费者同步问题(当然了,Channel是不移动的)。

如果一个Channel的容量是 1,那么当一份数据被发送到该Channel之后,就必须要等待这份数据被取出之后,下一份数据才能再被发送进来,发送者与接收者每一次只能在一个时间传送一份数据,而且任何一方都必须要等待另一方执行完成之后才能传送或者接收下一个份数据。我们的示例将从同步Channel开始。

我们到现在为止所定义的任何一个Channel都是同步的——新数据必须在旧数据被取走后才能传送进去,现在让我们来看看这个蛋糕制作工厂在Go是如何实现的:

package main

import (
    "fmt"
    "time"
    "strconv"
)

var i int

func makeCakeAndSend(cs chan string) {
    i = i + 1
    cakeName := "Strawberry Cake " + strconv.Itoa(i)
    fmt.Println("Making a cake and sending...", cakeName)
    cs <- cakeName // send a strawberry cake
}

func receiveCakeAndPack(cs chan string) {
    s := <- cs // get whatever cake is on the channel
    fmt.Println("Packing received cake: ", s)
}

func main() {
    cs := make(chan string)
    for i := 1; i < 3; i++ {
        go makeCakeAndSend(cs)
        go receiveCakeAndPack(cs)
    }
    // Sleep for a while so that the program doesn't exit immediately and   output is clear for illustration
    wait, _ := time.ParseDuration("2s")
    time.Sleep(wait)
}

输出结果为:

Making a cake and sending ... Strawberry Cake 1
Packing received cake: Strawberry Cake 1
Making a cake and sending ... Strawberry Cake 2
Packing received cake: Strawberry Cake 2
Making a cake and sending ... Strawberry Cake 3
Packing received cake: Strawberry Cake 3

上面的代码中,我们进行了三次请求来制作一个蛋糕,并立马将其打包,。因为我们的生产方(makeCakeAndSend)与消费方(receiveCakeAndPack)是同步的,所以在生产方生产好蛋糕之后必须等待消费方接收数据,它才能再一次往Channel中发送数据。

现在我们来改变一下下代码,通常的,Goroutines是一个在一直重复着运行的代码块,它们对数据进行操作并且通过Channels与其它的Goroutines进行数据的传送,在下一个示例中,我们将上面示例的循环移动到Goroutine里面去,这样我们只请求Goroutine一次:

package main

import (
    "fmt"
    "time"
    "strconv"
)

func makeCakeAndSend(cs chan string) {
    for i := 1; i<=3; i++ {
        cakeName := "Strawberry Cake " + strconv.Itoa(i)
        fmt.Println("Making a cake and sending...", cakeName)
        cs <- cakeName
    }
}

func receiveCakeAndPack(cs chan string) {
    for i := 1; i<=3; i++ {
        s := <- cs
        fmt.Println("Packing received cake: ", s)
    }
}

func main() {
    cs := make(chan string)
    go makeCakeAndSend(cs)
    go receiveCakeAndPack(cs)

    wait, _ := time.ParseDuration("5s")
    time.Sleep(wait)
}

输出为:

Making a cake and sending... Strawberry Cake 1
Making a cake and sending... Strawberry Cake 2
Packing received cake:  Strawberry Cake 1
Packing received cake:  Strawberry Cake 2
Making a cake and sending... Strawberry Cake 3
Packing received cake:  Strawberry Cake 3

上面的输出只是在我电脑上面的,但是你的并不一定也是这样一模一样的输出,由于我们将循环移到了Goroutines里面,所以对于 makeCakeAndSend 和 receiveCakeAndPack 会分别制作三个蛋糕并传送到Channel上和取出三个蛋糕并打包。

Goroutines 允许您能够并行的执行任务——为什么又出来了这么一个新词儿?虽然现在已经有很多形容并行的词了,比如线程、协程、进程等等,但是没有一个已有的词汇能精确地表达出Goroutines的内涵,一个Goroutines就是一个与其它的Goroutine在同一地址空间中并行执行的函数或者方法,一个运行中的程序有一个或者多个Goroutines组成,Go官方文档如下面这么说道:

Goroutines被复用到多个系统线程上,所以,如果某一个Goroutine阻塞了,并不会影响其它的Goroutine。 Goroutines为我们隐藏了很多的内部的并行机制,这样的设计让语言的设计者可以根据需要修改它底层并行的实现,以改进其运行性能或者进行硬件优化,而作为语言使用者的我们则不需要修改任何一个代码,就能获得性能的提升。

在大多数程序中,我们都是按顺序执行的,因为作为用户的人的处理能力是远远不及计算机的,比如当我们在写代码的时候,我们敲击键盘的速度比计算机处理敲击的速度是慢得多的,所以,计算机在这种情况下一直是处于等待我们的输入的状态。但是对于一台每一秒都处理成千上万请求的服务器来说,这种顺序执行的方法显然是不可能的,如果一百个人同时请求,我们不可能按顺序的执行每一个用户的请求,这个时候就需要并发,在Go中,我们使用Goroutines。

Goroutines与普通的函数没有什么不一样的,只是它执行它的时候,需要在函数的名称前面使用一个 go 关键字,如果我们 myFunc() 是一个方法,那么如果要让它以一个Goroutine的方式执行,只需要 go myFunc() 即可——这将使得它以一个新的线程运行,让我们来做一个简单的示例:

package main
import "fmt"
func LoopIt(times int) {
    for i := 0; i < times; i++ {
        fmt.Printf("%dt", i)
    }
    fmt.Println("--Loop End--")
}
func main() {
    LoopIt(10)
    LoopIt(10)
}

输出为:

0   1   2   3   4   5   6   7   8   9   --Loop End--
0   1   2   3   4   5   6   7   8   9   --Loop End--

然后我们对程序做一个很小的改动:

//...
func main() {
    go LoopIt(10)
    LoopIt(10)
}

它的输出结果可能就成为:

0   0   1   2   1   3   2   4   3   5   4   6   5   7   6   8   7   9   8   --Loop End--
--Loop End--

为什么会有这样的区别?因为当我启动一个 Goroutine 之后,LoopIt这个函数立马就会执行,但是它并不会阻塞整个程序,所以,它开始执行之后,它后面的函数也会开始执行,后面的函数就是一个普通的函数,所以它会顺序执行,这使得两人个函数同时运行了,所以我们上面的输出中,这样一来,LoopIt 会一直输出数字,而由于go LoopIt也在执行,它也会输出数据,所以就出现了上面那种情况,这两人个 LoopIt并行了。

再来看看下面这个例子:

package main
import (
    "fmt"
    "time"
)
func simulateEvent(name string, timeInSecs string) {
    fmt.Println("Started ", name, ": Should take", timeInSecs, "seconds")
    du, _ := time.ParseDuration(timeInSecs)
    time.Sleep(du)
    fmt.Println("Finished ", name)
}
func main() {
    simulateEvent("First", "10s")
    simulateEvent("Second", "6s")
    simulateEvent("Third", "3s")
}

输出结果为:

Started  First : Should take 10s seconds
Finished  First
Started  Second : Should take 6s seconds
Finished  Second
Started  Third : Should take 3s seconds
Finished  Third

这是一个顺序执行的程序,所以,我们可以看到,必须是第一个函数执行完了之后才执行第二个函数,但是我们再来看看下面这个示例:

package main
import (
    "fmt"
    "time"
)
func simulateEvent(name string, timeInSecs string) {
    fmt.Println("Started ", name, ": Should take", timeInSecs, "seconds")
    du, _ := time.ParseDuration(timeInSecs)
    time.Sleep(du)
    fmt.Println("Finished ", name)
}
func main() {
    go simulateEvent("First", "10s")
    go simulateEvent("Second", "6s")
    go simulateEvent("Third", "3s")
    wait, _ := time.ParseDuration("15s")
    time.Sleep(wait)
    fmt.Println("Exited")
}

输出为:

Started  First : Should take 10s seconds
Started  Second : Should take 6s seconds
Started  Third : Should take 3s seconds
Finished  Third
Finished  Second
Finished  First
Exited

所有的函数都同时运行了,然后因为第三个函数只暂时3秒,所以它最先运行完成,而第二个第二完成,第一个因为要暂时10秒,所以最后完成,我们在 main() 函数中使用暂时了程序15秒,以让上面的三个 Goroutines运行完成之后才退出程序。

多态,按字面意思来讲就是多种状态,在面向对象编程语言中,接口的多种不同的实现方式即为多态。引用Charlie Calverts对多态的描述——多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作(摘自“Delphi4 编程技术内幕”)。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。多态性在Object Pascal和C++中都是通过虚函数(Virtual Function) 实现的。

还是用简单的例子来讲明什么要多态吧,我们有一个名为“人”的接口,它有一个方法“我喜欢”,然后一个从火星来的朋友就是程序的使用者,它来到地球之后先问了一个男人:“你喜欢做什么?”,男人调用“我喜欢”这个方法返回给他一个值:“钓鱼”;然后火星人又去问了一个女人同样的问题:“你喜欢做什么?”,然后女人也实现了“人”这个接口,她也调用“我喜欢”这个方法返回给火星人一个值:“购物”……如此这么问下去,发现,每一个人对:“你喜欢做什么?”这个问题的问题都是不一样的,而这种不一样,就是*多态*——从同一种类型的同一个方法中获取完全不同的结果——在这个例子中,接接口就是“人”。

Go中,我们使用接口来实现多态,如果你看这篇文章时,还不了解Go的接口,建议你先看看我的另一篇文章Go 中的接口,然后再返回来看本文。创建我们创建了一个接口,并且有其它的类型实现了这个接口,那么我们就可以通过这个接口直接访问到每一个类型中所定义的那个方法,而不需要再知道它具体是什么类型,让我们用Go来实现上面那个关于火星人问问题的例子:

package main
import "fmt"
// 定义人这个接口
type Human interface {
    // 它有一个方法 iLove
    iLove() string
}
// 定义男人
type Man struct {}
// 实现人这个接口
func (man Man) iLove() string {
    return "钓鱼"
}
//重复上面的定义
type Woman struct {}
func (woman Woman) iLove() string {
    return "购物"
}
func main() {
    m := new (Man)
    w := new (Woman)
    // 一个保存人的数组,它有两人个元素
    // 一个为 Man 类型,一个为Woman类型
    humans := [...]Human{m,w}
    for i, _ := range (humans) {
        // 直接调用接口的 iLove 方法
        fmt.Println("我喜欢", humans[i].iLove())
    }
}

输出为:

我喜欢 钓鱼
我喜欢 购物

在上面的例子了,我们都是调用了 Human 的 iLove 方法,但是得到了两人个结果,而且我们是直接使用 for 循环来对保存了 Human 数据的数组进行遍历而不是明显地使用 Man.iLove() 方法或者Woman.iLove() 方法。

现在又有一个新问题产生了,这火星人并不知道在地球上我们把人和其它生物是作不同对待的,所以,他还跑去问了一只狗:“你喜欢做什么啊”?狗很客气的“汪!汪汪!汪汪汪!”,那这种新情况出现了我们该怎么办?好办,让我们再来扩展一下上面的例子:

package main
import "fmt"
// 定义生物这个接口
type Organism interface {
    // 它有一个方法 iLove
    iLove() string
}
// 定义人这种生物
type Human struct {}
// 定义人对生物这个接口的实现
func (human Human) iLove() string {
    return "做人该做的事情,我是一只人,不是一个狗!"
}
// 定义男人
type Man struct { Human }
func (man Man) iLove() string {
    return "钓鱼,我是一只男人。"
}
//重复上面的定义
type Woman struct { Human }
func (woman Woman) iLove() string {
    return "购物,我是一只女人。"
}
// 定义狗
type Dog struct {}
func (dog Dog) iLove() string {
    return "汪!汪汪!汪汪汪!我是一个狗!不是一只人。"
}
func main() {
    h := new (Human)
    m := new (Man)
    w := new (Woman)
    d := new (Dog)
    organisms := [...]Organism{h, m, w, d}
    for i, _ := range (organisms) {
        // 直接调用接口的 iLove 方法
        fmt.Println("我喜欢", organisms[i].iLove())
    }
}

输出为:

我喜欢 做人该做的事情,我是一只人,不是一个狗!
我喜欢 钓鱼,我是一只男人。
我喜欢 购物,我是一只女人。
我喜欢 汪!汪汪!汪汪汪!我是一个狗!不是一只人。

在我那篇《Go 中的接口》的文章中,我用了一个很简单的示例来讲明Go的接口实现——矩形与方形同时实现了形状接口。但是我们要清楚的认识到,Go的接口并不是一种C或者Java的变体,而是具有了很多的特性,它是大规模程序开发及高可用性程序开发所需要的关键。

先看看Java里面接口的实现方法,然后再返回来看看Go的接口的实现方式,我们就能很容易发现为什么说Go的接口实现可用性可扩展性更高,而且进化到另一个层次。我们的数据模型是 *Bus*,它同时实现了两人个接口——只有可用空间的立方体以及可以运输乘客的运输工具:

// 第一步:在编码之前,先设计好接口以及类的继承
// 第二步:将现实世界中的对象抽象成为我们可以在程序中使用的接口
interface PublicTransport {
    int PassengerCapacity();
}
interface Cuboid {
    int CubicVolumn();
}
// 第三步:创建数据结构同时去实现我们在第二步中定义的所有接口
public class Bus implements
PublicTransport,
Cuboid {
    // 定义类的数据结构
    int l, b, h, rows, seatsPerRow;
    public Bus(int l, int b, int h, int rows, int seatsPerRow) {
        this.l =l; this.b = b; this.h = h; this.rows = rows;
        this.seatsPerRow = seatsPerRow;
    }
    // 定义方法的实现
    public int CubicVolumn() { return l*b*h; }
    public int PassengerCapacity() { return rows * seatsPerRow; }
    // 在 main 函数中调用类及其方法
    public static void main() {
        Bus b = new Bus(10, 6, 3, 10, 5);
        System.out.Println(b.CubicVolume());
        System.out.Println(b.PassengerCapacity());
    }
}

在上面的Java示例中,你必须在实现接口方法之前明确地定义类的数据层次结构,并且需要使用implements 这个关键字,C# 中则是将接口名称在 : 符号后面列出来代替 implements 关键字,在这种情况下,任何这部分的需求的变更都会影响到项目的核心模块。

绝大数的项目就是在这种先定义接口然后去实现最后发现不能完全需求,又返回去修改接口中再实现再修改,如此往复,直到接口已经考虑到各方面的因素/涵盖了所有需求——而如果此时项目的需求还在变化的话,那工作量就更大了。在完全确定了接口之后,就不再推荐修改这些最底层的接口了,即使是项目管理员也不允许随意的修改它,而确实需要修改的话,有可能还需要通过各种各样大大小小的项目组研讨会等最后才能确定是否修改以及如何修改,因为接口做为最底层,它的一点修改,都会影响到上层的实现,如果一个接口有一百个实现,那么它的修改将需要对其一百个实现也做相应的修改。或者还有另一种方法,那就是重新创建一个新的接口,接口的定义就是现在这个接口修改后的定义,然后再创建新的类去实现新的接口。当我们的项目达到某一个层次之后,发现所有的这些事情已经完全失控了。

那有没有更好的办法?我们先来看看上面这个Java示例的的Go的实现:

package main
import "fmt"
// Go第一步:定义你的数据结构
type Bus struct {
    l, b, h int
    rows, seatsPerRow int
}
// Go第二步:定义一个现实世界对象的抽象层
type Cuboider interface {
    CubicVolume() int
}
// Go第三步:实现接口的方法
func (bus Bus) CubicVolume() int {
    return bus.l * bus.b * bus.h
}
// 重复上面的二三步
type PublicTransporter interface {
    PassengerCapacity() int
}
func (bus Bus) PassengerCapacity() int {
    return bus.rows * bus.seatsPerRow
}
func main() {
    bus := Bus{
        l:10, b:6, h:3,
        rows: 10, seatsPerRow: 5}
    fmt.Println("Cubic volume of bus: ", bus.CubicVolume())
    fmt.Println("Maximum number of passengers:", bus.PassengerCapacity())
}

在上在面的Go的实现中,所使用的名词与Java版本都一样,但是这之间却还是有很多的不一样,比如最直接的区别是我们看不到 implements 这种在Java中用来定义类的接口实现的关键字了。另外,Go中,看起来似乎是以数据为中心的,你先定义了你的数据结构,然后定义它的接口,而这种接口与实现之间的层次结构是没有明确说明的,我们没有使用特别的签名来关联接口与实现,而是Go根据语法规则自动的为你实现了接口。在这个示例中,似乎我们看不到Go的实现方法有任何的好处,但是当项目越来越大,或者需求变更时,Go的方法将被证明是更好的。

让我们假设,随着时间的发展,一个项目要求我们的巴士——法律要求,每一个乘客都应该有至于自己的最低限度的立体空间,因为我们之前只是按座位计算可乘客人数,而现还需要考虑汽车的空间以及乘客所能分配到的平均空间量。

那么,我们的汽车需要实现一个新的接口 PersonalSpaceLaw ,它的方法在任何其它的接口都是没有被实现过的(我们不得不这么做,以适应新的法律要求):

// 新的需求
interface PersonalSpaceLaw {
    boolean IsCampliantWithLaw();
}

class Bus implements
PublicTransport,
Cuboid,
PersonalSpaceLaw {
    //.....
    public IsCampliantWithLaw() {
        return (l * b * h ) / ( rows * seatsPerRow ) > 3;
    }
    //......
}

该需要的改变需要我们修改类的结构,添加新的实现的方法,而该类修改之后,所有继承Bus的子类都需要做相应的修改以适应新的Bus结构,但是让我们来看看Go的方法,只需要在代码中加入下面这些即可,不需要对其它已有的代码做任何修改:

// 新的项目需求
type PersonalSpaceLaw interface {
    IsCampliantWithLaw() bool
}
func (bus Bus) IsCampliantWithLaw() bool {
    return ( bus.l * bus.b * bus.h ) / (bus.rows * bus.seatsPerRow) >= 3
}

完整代码为:

package main
import "fmt"
// Go第一步:定义你的数据结构
type Bus struct {
    l, b, h int
    rows, seatsPerRow int
}
// Go第二步:定义一个现实世界对象的抽象层
type Cuboider interface {
    CubicVolume() int
}
// Go第三步:实现接口的方法
func (bus Bus) CubicVolume() int {
    return bus.l * bus.b * bus.h
}
// 重复上面的二三步
type PublicTransporter interface {
    PassengerCapacity() int
}
func (bus Bus) PassengerCapacity() int {
    return bus.rows * bus.seatsPerRow
}
// 新的项目需求
type PersonalSpaceLaw interface {
    IsCampliantWithLaw() bool
}
func (bus Bus) IsCampliantWithLaw() bool {
    return ( bus.l * bus.b * bus.h ) / (bus.rows * bus.seatsPerRow) >= 3
}
func main() {
    bus := Bus{
        l:10, b:6, h:3,
        rows: 10, seatsPerRow: 5}
    fmt.Println("Cubic volume of bus: ", bus.CubicVolume())
    fmt.Println("Maximum number of passengers:", bus.PassengerCapacity())
    fmt.Println("Is campliant with law:", bus.IsCampliantWithLaw())
}

输入结果为:

Cubic volume of bus: 180 Maximum number of passengers: 50 Is campliant with law: true

接口是一种存在于具体实现与预期实现之间的约定,接口定义了某一种函数或者方法应该怎么执行,需要接收什么样的参数,返回什么样儿的结果,而由各种不同的函数或者方法去做具体的实现,每一个实现同一个接口的函数则不需要做完全一样的处理。

以我们的生活来做一个比法,买东西付钱这是一个接口,但是具体去实现这个接口的人,则有可能会出现各种各样不同的实现方式,比如我刷卡支付,而你却是现金支付,还可能有一个他使用的是银行汇款,但是不管使用的是什么方式,我们都实现了买东西付钱这个接口。

在软件中,接口遵循与我们生活中的这种现像类似的模式,本文我将介绍在Go中如何有别于其它的如 C/C++、Java等面向对象的编程语言不一样的方法来实现接口。Go的接口的实现虽然与其它你所熟悉的语言有很大的不同,但是它工作得却非常的好。

我们的示例的需求是求各种各样形状的面积,比如矩形/方形/圆等等,因为形状可以做为一个接口,它只是一个抽象的概念,而实际形状,则是形状接口的不同实现,我们我们就可以获得一个统一的获取不同形状的面积的方法。如果你还没有任何编程经验,那可能到现在为止对接口的理解可能还是有一些模糊,但是你应该要知道接口是一种十分强大的编程思想——想想,在全世界都有同样的一个需求的时候,我们定义了一个统一的接串口,这样,你不管走到哪里都能以同样的方式完成,虽然有可能完成的过程不一样,但是你总是知道给了钱就可以拿到自己购买的商品,

Go Structs 中的方法Go 中的 Struct——无类实现面向对象编程我们已经有过对长方形面积的求取方式,我们创建了一个名为 Rectangle 的Struct,然后绑定了一个 Area函数到Rectangle 上来求取Rectangle实例的面积。但是代码却没有接口,所以,Area方法只能被 Rectangle使用,现在让我们将其扩展成为接口。

package main
import "fmt"
// Shaper是一个接口,它只有一个函数 Area返回一个int类型的结果
type Shaper interface {
    Area() int
}
type Rectangle struct {
    length, width int
}
// 该 Area 函数工作于 Rectangle 类型,同时与 Shper有同样的定义,现在就称为Rectangle实现了Shaper接口
func (r Rectangle) Area() int {
    return r.length * r.width
}
func main() {
    r := Rectangle{length: 5, width: 3}
    fmt.Println("Rectangle r details are: ", r)
    fmt.Println("Rectangle r's area is: ", r.Area())
    s := Shaper(r)
    fmt.Println("Area of the Shape r is: ", s.Area())
}

输出为:

Rectangle r details are:  {5 3}
Rectangle r's area is:  15
Area of the Shape r is:  15

在如 Java 或者C#这些语言中,一个类型或者类如果需要实现某个接口,需要这样的:

public class Rectangle implements Shaper { // implementation here }

但是在Go中,我们并不需要像上面Java或者C#那样声明一个实现,你可以看到代码中,Shaper接口仅仅只是被定义了,它有一个方法 Area() int*,而 Rectangle 类型也有一个与 Shaper 中 Area 完全一样的方法声明,名称与返回类型以及接收的参数(这里不接收任何参数)都是一样的,所以Go自动的就明白了 Rectangle 实现了 Shaper 接口,所以,你可以将 Rectangle 丢 Shaper ,像这样的: *s := Shaper®,这样使用 *s.Area() 同样也可以得到该形状的面积了。

上面的其实与我们以前写的示例是一样的,现在来增加一个状态 Square :

package main
import "fmt"
// Shaper是一个接口,它只有一个函数 Area返回一个int类型的结果
type Shaper interface {
    Area() int
}
type Rectangle struct {
    length, width int
}
// 该 Area 函数工作于 Rectangle 类型,同时与 Shper有同样的定义,现在就称为Rectangle实现了Shaper接口
func (r Rectangle) Area() int {
    return r.length * r.width
}
type Square struct {
    side int
}
func (sq Square) Area() int {
    return sq.side * sq.side
}
func main() {
    r := Rectangle{length: 5, width: 3}
    fmt.Println("Rectangle r details are: ", r)
    fmt.Println("Rectangle r's area is: ", r.Area())
    s := Shaper(r)
    fmt.Println("Area of the Shape r is: ", s.Area())
    sq := Square{5}
    fmt.Println("Square details is: ", sq)
    fmt.Println("Area of square sq is: ", sq.Area())
    sqs := Shaper(sq)
    fmt.Println("Shaper sqs area is: ", sqs.Area())
}

结果输出为:

Rectangle r details are:  {5 3}
Rectangle r's area is:  15
Area of the Shape r is:  15
Square details is:  {5}
Area of square sq is:  25
Shaper sqs area is:  25

在上面的示例中,Square 与 Rectangle 都实现了 Shaper 接口,虽然 Square 只有一个字段 side,而 Rectangle 有 length 与 width 两个,但是 Shaper 却知道在使用 Square.Area() 时,具体使用哪一种计算方法,下面我们来看一个更简单的输出,因为 Square 与 Rectangle 都实现了 Shaper 接口,所以我们可以将上面程序的 main() 函数修改成为下面这样的:

func main() {
    r := Rectangle{length: 5, width: 3}
    q := Square{side:5}
    shapes := [...]Shaper{r, q}
    fmt.Println("Looping through shapes for area ...")
    for n, _ := range shapes {
        fmt.Printf("Shape detail: %v , and is area is: %dn", shapes[n], shapes[n].Area())
    }
}

它的输出为:

Shape detail: {5 3} , and is area is: 15
Shape detail: {5} , and is area is: 25

继承 让一个类型自动的获取到它的父类的所有行为与属于,而多继承则使得一个类型可以自动的获取到多个父类的所有行为与属于,比如,以前我们有相机,后来有了手机,再到现在,我们所有人用的都是带相机的手机,那么,相机是一个父类,它具有属于相机所特有的属性与行为,比如它有镜头,可以拍照片;手机又是一个父类,它具有相机没有的行为,比如打电话,发短信,但是它又不具有相机的一些行为,比如拍照片等;而我们的相机手机,它同时继承自相机与手机,所以它就同时具有了打电话,发短信而且还能拍照片的功能。

在其它语言中,我们通常都是通过定义接口的方式来实现这种能力,但是在Go中,我们与单继承的作法是一模一样的,继承多少父类,就只需要多少个匿名的父类字段即可,比如上面关于手机、相机以及相机手机的例子,使用Go来实现的话,可以是这样的:

package main
import "fmt"
type Camera struct {}
func (_ Camera) takePicture() string {
    return "Clicked"
}
type Phone struct {}
func (_ Phone) call() string {
    return "Ring Ring"
}
type CameraPhone struct {
    Camera
    Phone
}
func main() {
    cp := new(CameraPhone)
    fmt.Println("A CameraPhone can take a picture: ", cp.takePicture())
    fmt.Println("A CameraPhone also can make a call: ", cp.call())
}

输出结果为:

A CameraPhone can take a picture:  Clicked
A CameraPhone also can make a call:  Ring Ring

我们可以看到,CameraPhone*的实例*cp*同时具有了 *Camera 的 takePicture 方法,也具有了Phone 的 call 方法。

如果你是一个有着其它面向对象语言开发经验的开发者,那么肯定知道什么是继承与子类了,简单来说,他就是一种类型继承另一种类型的方法、行为与属性,继承者具有被继承者全部的方法与属性,比如一个公司的“职员”它继承人大类“人”,职员具有人的所有属性与行为,一辆法拉利具有一辆人所具有的所有属性等等例子数不胜数,但是虽然阿斯顿·马丁也具有一辆车的所有属性与行为,却与法拉利是不一样的。

我们定义一辆车的行为,那么法拉利与阿斯顿·马丁都会具有这种行为,比如“车都是具有发动机的”或者“车都有方向盘或者其它能控制方向的东西”,但是如果我们定义了法拉利的行为和属性则并不一定会导致车或者阿斯顿·马丁也具有同样的行为与属性,比如1947年5月25日,法拉利自己设计并制造的运动车获罗马最高奖,但是这并不代表阿斯顿·马丁也有着同样的这个历史属性。

从上面这些示例中可以看到,子类继承父类的一些行为与属性,子类的行为与属性不一定会同时在父类上也体现,也并不一定会体现在继承自同一父类的其它子类上,我们将上面关于法拉利或者阿斯顿·马丁与汽车的例子体现到程序上面来,那将是这样的一种情况:我们定义一个Struct,其名为Car*,它具有一个方法 *numberOfWheels() 可以获取到车的轮胎数量,我们再定义一个StructFerrari 继承自 Car ,那么我们就可以使用 Ferrari.numberOfWheels() 方法来获取到 Ferrari 的轮胎数量。

我以前的文章Go Struct中的匿名字段 与 Go Structs 中的方法 介绍过,通过匿名字段可以实现上面的要求:

package main
import "fmt"
type Car struct {
    wheelCount int
}
// 定义 Car 的行为
func (car Car) numberOfWheels() int {
    return car.wheelCount
}
type Ferrari struct {
    Car // 匿名字段 Car
}
func main() {
    f := Ferrari{Car{4}}
    fmt.Printf("一辆法拉利有 %d 个轮胎n", f.numberOfWheels())
}

输出为:

一辆法拉利有 4 个轮胎

在上面的示例中,我们仅仅只定义了 Car 的行为或者方法,但是当我们将Car作为一个匿名字段作为Ferrari的属性是,Ferrari就自动的具有了Car的所有方法。那么我们如果再创建一个新的 Struct :AstonMartin 其结果是怎么样的呢?

package main
import "fmt"
type Car struct {
    wheelCount int
}
// 定义 Car 的行为
func (car Car) numberOfWheels() int {
    return car.wheelCount
}
type Ferrari struct {
    Car // 匿名字段 Car
}
// 定义只有 Ferrari 专属的方法
func (f Ferrari) sayHiToSchumacher() {
    fmt.Println("Hi Schumacher!")
}
type AstonMartin struct {
    Car
}
//添加只有 AstonMartin 专属的方法
func (a AstonMartin) sayHiToBond() {
    fmt.Println("Hi Bond, James Bond!")
}
func main() {
    f := Ferrari{Car{4}}
    fmt.Printf("一辆法拉利有 %d 个轮胎n", f.numberOfWheels())
    f.sayHiToSchumacher()
    a := AstonMartin{Car{4}}
    fmt.Printf("一辆阿斯顿马丁有 %d 个轮胎n", a.numberOfWheels())
    a.sayHiToBond()
}

输出为:

一辆法拉利有 4 个轮胎
Hi Schumacher!
一辆阿斯顿马丁有 4 个轮胎
Hi Bond, James Bond!

在上面示例中,AstonMartin 与 Ferrari 同时具有 numberOfWheels() 方法,并且又都具有它们自己的方法,但是像下面这样的就不行:

func main() {
    f := Ferrari{Car{4}}
    fmt.Printf("一辆法拉利有 %d 个轮胎n", f.numberOfWheels())
    f.sayHiToBond()
    a := AstonMartin{Car{4}}
    fmt.Printf("一辆阿斯顿马丁有 %d 个轮胎n", a.numberOfWheels())
    a.sayHiToSchumacher()
}

输出为:

./inherit3.go:27: f.sayHiToBond undefined (type Ferrari has no field or method sayHiToBond)
./inherit3.go:30: a.sayHiToSchumacher undefined (type AstonMartin has no field or method sayHiToSchumacher)