分类 文章 下的文章

在本文中,我们将看看如何使用一个来自Go以外的API创建应用,我们将使用Google提供的URL缩短服务API来创建URL地址的缩短版本,你可以在http://goo.gl/体验一把,比如你现在可以通过http://goo.gl/FE6F2 这个地址来访问本文,goo.gl 还会生成一个你的网址的二维码,比如本文的二维码是:

在Go网络编程中使用外部API——基于 Google API 创建URL地址缩短服务

这样的服务是很有用的,比如本文的网址,你应该也看到了,不管是让你记下来或者是让你写下来都太难了,但是缩短之后不管怎么样都简单得多,这么好的服务,Google以API的形式给大家免费使用,你可以在https://developers.google.com/url-shortener/ 了解更多有关该API的信息。

我们要写的程序十分的很简单,同时,我还想在写了这个小程序之后再将它发布到 Google的AppEngine上面去,不过这都是后话了,整个程序需要用到的知识就下面这么几个:

第一步 确定已经正确的创建了开发环境

很简单:

cox@Cox:~/websites$ mkdir urlshortener
cox@Cox:~/websites$ cd urlshortener/
cox@Cox:~/websites/urlshortener$ export GOPATH=$(pwd)
cox@Cox:~/websites/urlshortener$ export PATH=$GOPATH/bin:$PATH
cox@Cox:~/websites/urlshortener$ mkdir pkg bin src

第二步 安装APIs

执行下面命令:

go get code.google.com/p/google-api-go-client/urlshortener/v1

第三步 在我们的代码中导入该API

上面安装的API可以通过下面这样导入到我们的项目中来了:

import "code.google.com/p/google-api-go-client/urlshortener/v1"

完成其它的代码

我的整个代码如下:

package main

import (
        "fmt"
        "net/http"
        "text/template"
        "code.google.com/p/google-api-go-client/urlshortener/v1"
)

// 本项目的网页模板
var tmpl = template.Must(template.New("URL Shortener Template").Parse(`
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><title>Goo.gl网址缩短服务</title>
<style>
input, button { border: .1em solid #ccc; border-radius: .3em; padding: .5em; }
button { background-color: #f0f0f0; }
button:hover { background-color: #ccc;}
</style>
</head>
<body>
<h1>Goo.gl网址缩短服务</h1>
{{if .}}{{.}}<br /><hr /><br />{{end}}
<form action="/shorten" type="POST">
<label for="long-url">长网址:</label><input type="text" name="url" id="long-url" placeholder="请在这里输入您要缩短的网址" /><button><span>给我短址</span></button>
</form>
<br /><hr /><br />
<form action="/lengthen" type="POST">
<label for="short-url">短网址:</label><input type="text" name="url" id="short-url" placeholder="请在这里输入您要获取原始网址的短网址" /><button><span>给我长址</span></button>
</form></body></html>
`))

func handleRoot(w http.ResponseWriter, r *http.Request) {
        tmpl.Execute(w, nil)
}

func handleShorten(w http.ResponseWriter, r *http.Request) {
        url := r.FormValue("url") // 获取由网页提交的网址
        svc, _ := urlshortener.New(http.DefaultClient) // 使用http中的default client创建一个新的 urlshortener 实例
        shorturl, _ := svc.Url.Insert(&urlshortener.Url { LongUrl: url, }).Do() // 填充长的网址然后呼叫缩短服务
        tmpl.Execute(w, fmt.Sprintf("<h2 class="url"><a href="%s">%s</a></h2><h3 class="url">源始长网址为:<em>%s</em></h3>", shorturl.Id, shorturl.Id, url))
}

func handleLengthen(w http.ResponseWriter, r *http.Request) {
        url := "http://goo.go/" + r.FormValue("url")
        svc, _ := urlshortener.New(http.DefaultClient)
        longurl, err := svc.Url.Get(url).Do()
        if err != nil {
                fmt.Println("error: %v", err)
                return
        }
        tmpl.Execute(w, fmt.Sprintf("<h2 class="url"><a href="%s">%s</a></h2><h3 class="url">短网址为:<em>%s</em></h3>", url, url, longurl))
}

func main() {
        http.HandleFunc("/", handleRoot)
        http.HandleFunc("/shorten", handleShorten)
        http.HandleFunc("/lengthen", handleLengthen)

        http.ListenAndServe(":8001", nil)
}

运行该程序之后您即可以打开浏览器,并访问:

本文原文为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)

我们知道 Go 中的 Struct 可以像其它语言中的类一样,包含数据,同样的,它也可以通过定义方法来保存属性它自己的行为,将一个方法绑定、关联到一个 Struct 或者任何一个其它的类型中是非常简单的,它就像普通的函数定义一样,唯一的不同是你需要为其指定一个类型(type)。

下面这个示例是一个普通的函数定义,它将返回一个类型为 int 的值:

func myFunc() int {
    // code
}

上面定义的函数将不接收任何参数,返回一个 int 类型的值,如果我们要将其绑定或者说关联到一个Struct上面的话,只需要做很少的修改即可:

type myType struct {}
func (m myType) myFunc() int {
    // code
}

让我们现在来扩展一下前面一篇文章《Go 中的 Struct——无类实现面向对象编程》中的那个 Rectangle 示例, 我们为其添加一个名为 Area 的方法,用来计算长方形的面积:

package main
import "fmt"
type Rectangle struct {
    width, length int
}
func (r Rectangle) Area() int {
    return r.width * r.length
}
func main() {
    r := Rectangle{4,3}
    fmt.Println("Rectangle is:", r)
    fmt.Println("Rectangle area is:", r.Area())
}

输出为:

Rectangle is: {4 3}
Rectangle area is: 12

许多面向对象的程序开发语言都有一个类似 this 或者 self 的概念,用它来代表并访问当前的实例,Go没有这样的概念,如果要将某一个函数或者方法绑定到一个Struct中,直接将一个Struct的实例作为参数传递给该方法或者函数,在上面的示例中,r 就是这个传递给方法 Area 的 Rectangle实例,它会在每一次调用 r.Area() 方法时,指向 r 自身。

同样的,你还可以传递Struct的引用,但是这与直接传递实例没有差别,因为Go会自动为你进行转换:

package main
import "fmt"
type Rectangle struct {
    length, width int
}
func (r Rectangle) AreaByValue() int {
    return r.length * r.width
}
func (r *Rectangle) AreaByReference() int {
    return r.length * r.width
}
func main() {
    r := Rectangle{4,3}
    fmt.Println("Rectangle is: ", r)
    fmt.Println("Rectangle area is: ", r.AreaByValue())
    fmt.Println("Rectangle area is: ", r.AreaByReference())
    fmt.Println("Rectangle area is: ", (&r).AreaByValue())
    fmt.Println("Rectangle area is: ", (&r).AreaByReference())
}

输出为:

Rectangle is:  {4 3}
Rectangle area is:  12
Rectangle area is:  12
Rectangle area is:  12
Rectangle area is:  12

在上面的代码中,我们定义了两人个很相似的方法,一个传递实例,一个传递实例的引用,我们同时又使用了实例分别调用它们,再使用了实例的地址调用它们,可以看到,四种方法的结果都是一样的。

下面我们再“绑定”一个名为 Perimeter 的方法来设置长方形的周长:

package main
import "fmt"
type Rectangle struct {
    width, length int
}
func (r Rectangle) Area() int {
    return r.width * r.length
}
func (r Rectangle) Perimeter() int {
    return 2 * (r.length + r.width)
}
func main() {
    r := Rectangle{4,3}
    fmt.Println("Rectangle is:", r)
    fmt.Println("Rectangle area is:", r.Area())
    fmt.Println("Rectangle perimeter is:", r.Perimeter())
}

输出结果为:

Rectangle is: {4 3}
Rectangle area is: 12
Rectangle perimeter is: 14

现在你可能会想到了,既然为自定义的Struct添加方法绑定,扩展它的行为这么简单,那是否我也可以很容易扩展以有的Struct的方法呢?看下面的示例:

package main
import "fmt"
import "time"
func (t time.Time) first3Chars() string {
    return t.Weekday().String()[0:3]
}
func main() {
    t := time.Time{}
    fmt.Println("First 3 Chars is: ", t.first3Chars())
}

输出为:

./structsmethods4.go:4: cannot define new methods on non-local type time.Time
./structsmethods4.go:9: t.first3Chars undefined (type time.Time has no field or method first3Chars)

你只能将方法或者函数绑定到相同的包内的Struct中,如果你真的需要扩展其它包,那再想想还有什么办法?其实很简单,那就是使用Go Struct中的匿名字段

package main
import (
    "fmt"
    "time"
)
type myTime struct {
    time.Time // 匿名字段
}
func (t myTime) first3Chars() string {
    return t.Weekday().String()[0:3]
}
func main() {
    m := myTime{}
    fmt.Println("Full Weekday:", m.Weekday().String())
    fmt.Println("First 3 chars:", m.first3Chars())
}

输出结果:

Full Weekday: Monday
First 3 chars: Mon

匿名字段中的方法

在《Go Struct中的匿名字段》这篇文章里面我讲到过了对匿名字段的方法,当 time.Time 成为了 myTime 的一个匿名字段之后,我们就能通过 myTime 访问 Time 所的方法,我们同样可以使用 myTime.String() 将自己转换成为一个字符串,让我们再来做看一看前面的示例。

在下面这个示例中,我们返回到Kitchen与House的问题上面来,我们知道了如何访问匿名字段的数据,那么也应该知道如何访问匿名字段的方法了,他们是一样的:

package main
import "fmt"
type Kitchen struct {
    numOfForks int
    numOfKnives int
}
func (k Kitchen) totalForksAndKnives() int {
    return k.numOfForks + k.numOfKnives
}
type House struct {
    Kitchen
}
func main() {
    h := House{Kitchen{4,4}}
    fmt.Println("Sum of forks and knives in house:", h.totalForksAndKnives())
}

输出结果为:

Sum of forks and knives in house: 8

Go 允许你定义含有无变量名的字段的Struct,这些无名称的字段称之为匿名字段,让我们通过一些示例来了解它们到底是什么,以及它们为什么很有用。

在下面的示例中,我们定义一个名为 Kitchen 的Struct,它仅仅只包含一个名为 numOfPlates 的 int类型的字段用来保存盘子的个数,我还定义了另一个Struct名为 House ,它包含了一个 Kitchen 的实例,但是我没有为其指定字段名,所以,它是一个匿名字段。

package main
import "fmt"
type Kitchen struct {
    numOfPlates int
}
type House struct {
    Kitchen // 匿名字段
    numOfRooms int
}
func main() {
    h := House{Kitchen{10}, 3} // 初始化需要使用Struct名
    fmt.Println("房屋 h 有", h.numOfRooms, "间房") // numOfRooms 是 House 的一个字段
    fmt.Println("房屋 h 有", h.numOfPlates, "个盘子") // numOfPlates 是Kitchen提供的字段,而Kitchen又是House的一个匿名字段,所以这里可以访问到它
    fmt.Println("这间房屋的厨房有:", h.Kitchen) // 我们可以通过Struct的名称访问整个匿名Struct的内容
}

输出结果为:

房屋 h 有 3 间房
房屋 h 有 10 个盘子
这间房屋的厨房有: {10}

首先我们需要注意到的事情是,当我们定义了 Kitchen 为House的一个匿名字段之后,我们将可以直接通过Kitchen中的字段名称访问Kitchen中字段的值,这同样也“成了”House的字段,但是如果像在Java这样的语言中,我们做一下对比,它需要像下面这样的去实现同样的功能:

public class Kitchen {
    public int numOfPlates;
}

public class House {
    public Kitchen kitchen;
}

public static void main(String[] args) {
    House h = new House();
    h.kitchen.numOfPlates = 10; // 引用自字段的子字段
}

其次我们需要知道,匿名字段的完整的实例数据是可以被访问到的,需要通过它的Struct名称,在本示例中,通过 h.Kitchen 来访问获得,所以,如果你想打印出 Kitchen 中的 Plate 数量的话中,可以这样:

fmt.Println(h.Kitchen.numofPlates)

最后,要注意到,初始化的时候,匿名字段的Struct名称是不允许省略的,它必须像这样:h := House{Kitchen{10}, 3}*,而下面这样的写法都是错误的:*h := House{{10}, 3} 或者 h := House{10,3}。

匿名字段发生命名冲突

很有可能两个不同的Struct具有同样名称的字段,而一个Struct又是另一个Struct的匿名字段,这就产生的命名冲突的问题,当这个问题出现的时候,外层的Struct中的字段可以像以前一样直接被访问到,但是其匿名字段就需要通过Struct名称访问了。

在下面的示例中,我们的 House 与 Kitchen 同时具有 numOfLamps 字段,但是House是外层的struct,它的 numOfLamps* 字段将隐藏掉Kitchen的 *numOfLamps*,如果你还是需要访问Kitchen的numOfLamps值,那么需要通过引用它的Struct名:*h.Kitchen.numOfLamps :

package main
import "fmt"
type Kitchen struct {
    numOfLamps int
}
type House struct {
    Kitchen
    numOfLamps int
}
func main() {
    h := House{Kitchen{2}, 10} // Kitchen 有2个Lamps,House有10个
    fmt.Println("House有", h.numOfLamps, "个Lamps")
    fmt.Println("Kitchen有", h.Kitchen.numOfLamps, "个Lamps")
}

输出为:

House有 10 个Lamps
Kitchen有 2 个Lamps

所以从上面可以看到,对于不同层级的相同名称的字段,Go制定了相应的规则,但是对于同一层级的相同名称的字段,则需要我们自己去解决,在下面的示例中,Kitchen与Bedroom都具有 numOfLamps 字段,并且它们又都是House的匿名字段,现在,如果我们引用 House.numOfLamps,Go编译器就会不知道你到底是要使用哪一个 numOfLamps值:

package main
import "fmt"
type Kitchen struct {
    numOfLamps int;
}
type Bedroom struct {
    numOfLamps int;
}
type House struct {
    Kitchen
    Bedroom
}
func main() {
    h := House{Kitchen{2}, Bedroom{3}} // Kitchen 2, Bedroom 3
    fmt.Println("Ambiguous number of lamps:", h.numOfLamps) // 这会出错
}

输出为:

./anonymousfields3.go:15: ambiguous selector h.numOfLamps

因为Go不知道你到底是要 Bedroom 的还是 Kitchen 的 numOfLamps,所以,它直接就提供值了,结果就是返回 h.numOfLamps 有歧义。

解决这个问题的办法是通过匿名字段的Struct名称访问其值:

fmt.Println("House中Lamps的数量为:", h.Kitchen.numOfLamps + h.Bedroom.numOfLamps)

输出为:

House中Lamps的数量为:5

星期五, 12/28/2012 - 16:22 — matrixstack

Go是非面向对象的,而是面向过程的,但是在当今程序开发的世界里,使用最为广泛的还是面向对象编程,这或许会让你感觉Go作为一个新生的语言最应该有的一面却没有被支持,但是其实它只是用了自己的方法,在本文里,我将介绍在Go里面,如何实现面向对象(Go提供了面向对象所提供的一切,或许提供得更多)。

如果你一直都是一个“类”用户,那么转到Go的“面向对象”上来,可能还需要一些观念上的转变,这或许很难,但是一旦你转变过来了,你就会发现,Go的方式有多么的强大了。

在了解Go的面向对象之前,一定要在心里面知道:Go是没有类的,这是你进入Go的面向对象世界的第一步,也是最重要的一步;另一个你知道知道的是:Go不是面向对象的,只是他的实现类似于面向对象,我们来看一个简单的示例,它能说明一切:

Java代码:

class House {
    public String getHouseName() { // 方法定义在类里面
        // 实现
    }
}

Go 代码:

type House struct { }
func (h House) GetHouseName() string {} //方法被定义在 Struct 外,但是却作用于 House

现在让我们来学习一下如何创建 Struct 以及如何通过它实现“面向对象”,一个 Struct 通过关键字 type 与 struct 定义:

type my_struct_name struct { }
type Rectangle struct { }
type Vehicle struct { }
type VehicleCar struct { }

上面的所有定义都是合法的,但是下面这样是不合法的:

type Hash# struct { } // 名称不允许包含特殊字符
type 0Struct struct { } // 名称不允许以数字开始

当然了,Struct 还能存储其它数据,所以,在这方面它类似于 Class,可以用来定义真实世界中存在的实体:

type mystruct struct {
    i int
    j int
    s string
}

type Rectangle struct {
    length, width int //你可以在一行中通过逗号分割的方式创建多个同类型的项目
    area float64
}

下面这个示例展示了一个 Struct 的实际应用:

package main
import "fmt"
// 定义 Rectangle 结构
type Rectangle struct {
    length, width int
}
func main() {
    r := Rectangle{}
    fmt.Println("默认的长方形是:", r) // 打印出默认的 Rectangle 的归零值
}

输出为:

默认的长方形是: {0 0}

这里有一个重要的需要注意的事情是:struct中的变量的值都是归零的,也就是说,int 类型的值将为 0,string 类型的值将为空字符串等等,所以,每一个你自己定义的Struct 也都是有一个属于其自己的归堆值的。下面这个示例中,我将展示另一种方式初始化 Struct——在创建它的时候就指定初始值:

package main
import "fmt"
type Rectangle struct {
    length, width int
    name string
}
func main() {
    r1 := Rectangle{2, 1, "First Rectangle"}
    fmt.Println("第一个长方形 r1 为:", r1)
    r2 := Rectangle{width: 3, name: "Second Rectangle", length: 4}
    fmt.Println("第二个长方形 r2 为:", r2)
    pr := new (Rectangle) // 获取一个指向一个新的长方形的指针
    (*pr).width = 6 // 设置 pr 的宽为 6
    pr.length = 8 // 与上面是同样的效果,看这里是没有 -> 符号的
    pr.name = "指针长方形"
    fmt.Println("指针长方形是:", pr)
}

输出为:

第一个长方形 r1 为: {2 1 First Rectangle}
第二个长方形 r2 为: {4 3 Second Rectangle}
指针长方形是: &{8 6 指针长方形}

这里需要注意的事情是:

  • 你可以只将值按 struct 定义时的顺序赋给它的成员,但是需要放在大括号里面,并且每一个值需要使用逗号分开;
  • 如果你给定值的时候还提供了名称,那么就不受顺序的限制;
  • 你可以通过 new 获取到一个新创建的 struct 的指针;
  • 这样的一个指针可以不需要 *操作符;
  • Go默认提供根据Struct的值转换成为字符打印的功能

封装以及 Struct 及其变量的访问

其它的编程语言使用一些特别的关键字(比如 public、private、package‘protected等)定义类中不同的属性与变量的可访问性,当我在使用Go之前,我认为所有的这些都是言存在的,但是之后,我发现,原来实现变量与属性的访问控制可以如此简单:

type notExported struct { // 以小写字母开头命名的struct只能在同一个包内调用,我们称这为不导出
}
type Exported { // 大写开头则可导出,也就是任何包都可以访问
    notExportedVariable int // 小写字母开头不允许包外访问
    ExportedVariable int // 大写字母开头则允许包外访问
    s string // 不导出
    S string // 导出
}

所以,你应该明白,在Go里面命名都应该是统一规定的,它要求你使用驼峰式的命名,是否使用大小写开头则要看是否需要导出。

for 声明是 Go 中唯一的一个循环声明,普通的用法如下:

for "initialization statements"; "bool expression that has to evaluate to true"; "statements run prior to every loop except the first" {
    // Code to be executed if the boolean expression evaluates to true
}

翻译过来为:

for "初始化声明"; "条件判断,为真是循环才执行"; "除第一外每一次循环都会执行的声明" {
    // 循环主体
}

上面的三个部分中,任何一个部分都是可以不写的,但是即使不写,那个属于它的结束符的分号却还是必须要写,除非三个声明都没有的时候,所有的分号都可以一并省略,请看下面的示例:

package main
import "fmt"
func main() {
    // 初始化i为0;每一次循环之后检查i是否小于5;让i加1
    for i := 0; i < 5; i++ {
        fmt.Println("i现在的值为:", i)
    }
}

执行输出结果为:

cox@Cox:~/workspace/go/src/gotutorial$ go run forloop1.go
i现在的值为: 0
i现在的值为: 1
i现在的值为: 2
i现在的值为: 3
i现在的值为: 4

从上面的结果可以看到,我们在定义 i 的初始值为0之后,在循环主体第一次执行前,i++是没有运行的,所以我们来能取得它的初始值 0。下面这些循环体你都可以在自己的示例中去运行:

// 没有判断声明,循环将永远不停的执行
for i := 0; ; i++ {
    fmt.Println("i现在的值为:", i)
}
// 因为 i 的值不会变化,而i的初始值小于3,所以,它将永远小于3,那么循环加永远运行
for i := 0; i < 3; {
    fmt.Println("i现在的值为", i)
}
// 虽然我们没有在 *for* 中声明初始值以及变量的变量方式,但是我们把初始化移到for循环外面,然后把初始变量的变化声明移动到循环里面也是可以的,下面代码的输出将为:
// s的值为:
// s的值为: a
// s的值为: aa
// s的值为: aaa
// s的值为: aaaa
s := ""
for ; s != "aaaaa"; {
    fmt.Println("s的值为:", s)
    s = s + "a"
}

在 Go 的 for 循环里面,你可以同时初始化多个变量,多个条件判断或者同时更新多个值,请看下面的示例:

package main
import "fmt"
func main() {
    for i, j, s := 0,5,"a"; i < 3 && j < 100 && s != "aaaaa"; i, j, s = i+1, j+1, s + "a" {
        fmt.Printf("i=%d, j=%d, s=%sn", i, j, s)
    }
}

输出:

i=0, j=5, s=a
i=1, j=6, s=aa
i=2, j=7, s=aaa

第三次循环之后,虽然 i < 100 与 s != "aaaaa" 都成立,但是 i < 3 已经不成立,所以循终止。

break 关键字

break 的作用是让循环在它所出现的地方终止,然后执行该循环后面的下一行代码或指令,如下示例:

package main
import "fmt"
func main() {
    i := 0
    for {
        if i >= 3 { break }
        fmt.Println("当前i的值为:", i)
        i++
    }
    fmt.Println("for后的第一个声明")
}

输出为:

当前i的值为: 0
当前i的值为: 1
当前i的值为: 2
for后的第一个声明

continue 关键字

continue 中止当前循环,然后回到循环的起始进行下一步循环,如下示例:

package main
import "fmt"
func main() {
    for i := 0; i < 11; i++ {
        if i%2 == 0 {
            continue //我不输入偶数
        }
        fmt.Println("当前i的值为:", i)
    }
}

输出为:

当前i的值为: 1
当前i的值为: 3
当前i的值为: 5
当前i的值为: 7
当前i的值为: 9

range 关键字

range 关键字让你可以遍历数据/Map,为了便于理解,你可以把它理解为“每一个XX的索引”,当你使用它对数组或者片段遍历时,每一次返回它们的索引,对于MAP遍历时,每一次返回它的键,如下示例:

package main
import "fmt"
func main() {
    // 对于数组,range 返回数据的索引
    a := [...]string{"a", "b", "c", "d"}
    for i := range a {
        fmt.Println("数组的第", i+1, "个值为:", a[i])
    }

    // 对于 map,返回每一个键
    capitals := map[string]string {"France":"Paris", "Italy":"Rome", "China":"Beijing" }
    for key := range capitals {
        fmt.Println("Capital of", key, "is", capitals[key])
    }
}

输出为:

数组的第 1 个值为: a
数组的第 2 个值为: b
数组的第 3 个值为: c
数组的第 4 个值为: d
Capital of Italy is Rome
Capital of France is Paris
Capital of China is Beijing

同时,range还可以同时返回两人个值,第一个值为索引或者键,第二个值为数组或者Map的值本身,比如上面的两人个循环我们可以改写成为下面这样也可以得到同样一输出:

package main
import "fmt"
func main() {
    // 对于数组,range 返回数据的索引
    a := [...]string{"a", "b", "c", "d"}
    for i,v := range a {
        fmt.Println("数组的第", i+1, "个值为:", v)
    }

    // 对于 map,返回每一个键
    capitals := map[string]string {"France":"Paris", "Italy":"Rome", "China":"Beijing" }
    for key,value := range capitals {
        fmt.Println("Capital of", key, "is", value)
    }
}

程序需要根据不同的条件进行不同的处理, 我们程序不可能只按着某一个顺序一路执行下去,比如我们经常会遇到的这样一种情况:如果某一个文件存在,则将新内容更新至已有文件,如果文件不存在,则先创建文件,这个时候我们就可以使用if - else 控制结构了,在Go中,该控制结构如下:

if some_boolean_expression {
    // 如果 some_boolean_expression 为 true,则会执行这里
} else if alternate_boolean_expression {
    // 如果 some_boolean_expression 为 false 并且 alternate_boolean_expression为 true,则执行这里
} else {
    // 如果上面的所有判断都为 false ,才能执行这里
}

注意事项 :

  • 在上面的结构说明,some_boolean_expression 以及 alternate_boolean_expression 的计算结果只能是 true 或者 false 里面的一个,不能为其它的值;
  • 对于判断声明,括号不是必须的,你可以为其添加一个括号,但是这是没必要的;
  • 与其它语言不同的是,Go要求左花括号必须与 if 或者 else 处于同一行内,就会报错,详情看写 GO 程序中一些常见的语法和其它错误
  • int 类型的数据不能作为判断声明的结果被 if-else使用。

下面这些是条件判断的比较操作符:

  • == : 等于
  • != : 不等于
  • < : 小于
  • <= : 小于或等于
  • > : 大于
  • >= : 大于或等于
  • && : 和(并且/与)
  • || : 或

下面是一个最很简单的示例:

package main
import "fmt"
func main() {
    if true {
        fmt.Println("这里会执行")
    }
    if false {
        fmt.Println("这里不会执行")
    }
}

输出很很简单,就是打印出“这里会执行”这五个字符,下面这个示例则使用了一些判断操作符:

package main
import "fmt"
func main() {
    a, b := 4, 5
    if a < b {
        fmt.Println("a < b")
    } else if a > b {
        fmt.Println("a > b")
    } else {
        fmt.Println("a = b")
    }
}

输入结果为 a < b,对于有其它语言编程经验的人来说,下面这些错误可能会经常出现:

// 非布尔值 5
if 5 {}
// 非布尔值 s
var s string
if s {}
// 非布尔值 e
var e os.Error
if e {}
// if 声明与左大括号不在一行内
if true
{}
// else 与 右大括号不在一行
if true {
}
else {
}

本文是 「Golang.org」的原文档翻译,出于本人有限的英语水平,如有不当,还请指正。

Go 是一个全新的语言,当然,它也从很多以存在多年的优秀语言中吸取了很多优秀的特性,但是因为其自身的特殊性,使得 Go 程序与其它相关的语言所开发的出来的程序有很大的不同,直接将C++或者Java程序翻译为Go程序并不会让你得到一个满意的结果——Java程序永远都只是Java程序,它成不了Go程序。另一方面,从Go的角度去想一个问题,你会得到一个可以成功解决它但是和其它语言完全不一样的方式。换句话说,想写出漂亮的Go程序,你必需要了解它的特性和风格,甚至是已有的约定,比如命名规范、代码格式化结构等,了解这些,可以让你的程序更简单地其它人看懂。

本文档给出了一些能让你写出更加干净的符合Go语言习惯的代码,但是建议你在阅读本文之前先阅读一下Language Specification、Tour of Go以及「如何写Go程序(How to Write Go Code)」.

示例

Go Package Sources 并不只包含了它的核心库,同时它还包含了很多使用该语言写的示例程序,如果你有关于如何使用Go解决问题或者在Go应该如何实现的相关问题,这些示例代码或话会给你答案,它还能告诉你,你应该将自己的想法以怎样的一种方式转换成Go程序。

格式化

格式化应该是最具有争议但是也是最重要的一个Go语言的特性,每一个开发者可以有属性自己的书写格式,但是所有人都使用同一种格式看起来肯定是最好的,这使得我们不再需要花费太多时间放在了解别人的书写风格上。

在Go中,不同于其它语言,它让计算机程序来管理着代码的格式化,gofmt 命令(也可以使用 go fmt)可以读取Go代码文件,并将它们以标准格式对其进行格式化,包括它们的缩进、空格等,请看下面这个示例:

type T struct {
    name string // name of the object
    value int // its value
}

gofmt 会将其格式化为:

type T struct {
    name    string  // name of the object
    value   int     // its value
}

所有Go标准库中的代码都已经通过gofmt进行了格式化。

比如下面这些Go中最常见的会使用到的格式化约定:

  • 缩进:Go使用制表符(Tabs)进行缩进,而不是很多语言中都推荐的空格,gofmt默认会使用制表符进行缩进,你应该仅仅在需要空格的地使用空格。
  • 行长度:Go没有行代度限制,所以,你完全不用担心这个问题,但是如果你感觉某一个行实在是太长了,那你可以将其截断,然后使用一些制表符让它看起来和它原本应该属于的那一行开始于同一个地方。
  • 括号:Go只有很少的一些地方需要括号,结构控制(比如if, for, switch等的条件判断)都是不需要括号的,操作符的优先级层次更短更清晰,所以,x << 1 + y < 6,就可以很明白的知道这是什么了。

注释

Go同时对G风格的块注释 /* ......*/ 和 C++风格的行注释 // .....提供支持,行注释是标准的,而块注释一般只建议使用的对包的注释上或者使用在需要禁用(但不是删除)一大段代码的功能上。

godoc程序(同时也是文档服务器)会从Go代码中提供包的说明文档,Comments that appear before top-level declarations, with no intervening newlines, are extracted alog with the declaration to serve as explanatory text for the item. The nature and style of these comments determines the quality of the documentation godoc produces.

任何一个包都应该包含一个包注释——一个用来说明包功能以及使用方法等信息的块注释,对于被分割为多个文件的包,包注释只需要出现在某中的任何一个文件中即可,godoc会将它作为所有同一个包的说明文档。The package comment should introduce the package and provide information relevant to the package as a whole. It will appear first on the godocpage and should set up the detailed documentation that follows.

/*
    Package regexp implements a simple library for
    regular expressions.

    The syntax of the regular expressions accepted is:

    regecxp:
        concatenation { '|' concatenation }
    concatenation:
        { closure }
    closure:
        term [ '*' | '+' | '?' ]
    term:
        '^'
        '$'
        '.'
        character
        '[' [ '^' ] character-ranges ']'
        '(' regexp ')'
*/
package regexp

如果某个包是一个十分简单的包,那么这个包的注释可以很简单:

// Package path implements utility routines for
// manipulating slash-separated filename paths.

注释不需要进行特殊的格式化,文档创建工作不能确定使用的是否是固定宽度的字体,所以,你不需要使用空格去对注释进行格式化——godoc像gofmt一样,它会帮助你完成这些格式化。注释应该仅仅保是纯文本的字符串,所以,HTML或者其它的标记方式(比如 THIS)都是不应该被使用的,godoc只会照字面的意思去解决,所以,请尽可能让你的注释内容就是你想表达的内容的文本——使用正确的拼写,合理的分段,长句进行合理的截段等。

在包内,任何一个仅跟着一个声明的注释都被当作该声明的注释文档,任何一个可导出的(大写开头的命名)的声明都应该具有一个说明注释,而这个注释文档最好也有遵循一定的约定,它的最第一句话应该是一个以该声明名称开始的用来概括该声明作用的句子。

// Compile parses a regular expression and returns, if successful, a Regexp
// object that can be used to match against text.
func Compile(str string) (regexp *Regexp, err error) {...}

Go 的声明语法允许分组声明,这使得一个单一的文档注释可以说明一组相关的常量或者变量,整个声明可以像下面这样来做(这是懒人做法啊):

// Error codes returned by failures to parse an expression.
var (
    ErrInternal     = errors.New("regexp: internal error")
    ErrUnmatchedLpar= errors.New("regexp: unmatched '('")
    ErrUnmatchedRpar= errors.New("regexp: unmatched ')'")
    ...
)

即使是私有名称,分组也是可以指明它们之间的关系的,比如下面这一组私有变量由一个互斥锁保护:

var (
    countLock   sync.Mutex
    inputCount  unit32
    outputCount unit32
    errorCount  unit32
)

名称

与其它语言一样,名称在Go中也是十分重要的,在某些环境下,一个名称甚至还需要其特有的语义——对于一个实例,它的方法或变量是否能被外部调用和访问取决于那个方法或变量的名称是否以大写字母开始,PublicItem这样的就是可以被外部访问的,而 privateItem则只能放在家里自己使用,也就是说:大写开始表示可以被外部访问,也就是可以被导出,而小写开头则只能是私有的。

包名称

当一个包被导入之后,包名称就成为一个访问该包资源的入口,比如,当我们:

import "bytes"

之后,我们就可以请求 bytes.Buffer了,一个简洁、明了令人回味的包名称可以帮助他人在使用该包的过程中更加方便快乐。我们约定包名称应该是一个“仅仅小写字母的单个字汇字符串”,我们不必要使用下划线或者驼峰式的写出一个完整的长的名称,你要想想,假如你自己在使用别人的某一个包时,需要像fooBarFooBar.FooBar这样请求它的资源,你会是一种什么样的感觉,我们只需要简单的foo.Bar即可。你不必要担心你所选择包名可能与其它已有的包名相同,因为Go允许在导入一个包时为其指定一个本地使用的别名。

另一个约定是“包名是该访问该包所有资源的基础入口”,比如 src/pkg/encoding/base64 包应该以encoding/base64导入,它的名称为 base64,而不是 encoding_base64或者encodingBase64。

包导入工具将使用名称来引用它的内容(import . 只在某些特殊用途的时候使用,如非必要,请尽可能的不去使用),所以,包中可导出的内容就可以以最直接方式访问到,对于实例,如bufio包中的Reader,就可以直接使用 bufio.Reader访问到,而不是 BufReader或者其它的名称,因为用户看前者会更直观。由于导入的实体总是以其完整的位置访问,所以 bufio.Reader不会与 io.Reader 混淆。相似的,用来创建新的 ring.Ring实例的构建函数,通常我们可能会将其命名为 NewRing,但是,因为 Ring 是 ring 导出的唯一一个类型,所以,我们也只能创建ring.Ring 这一种类型的数据,所以,直接将该构建函数取名为 New即可。

另一个*简化名称*的示例是 once.Do,我们不将其命名为 once.DoOrWaitUntilDown,长的名称在绝大多数时候并不会让我们更容易理解其所指代的意思,要让使用者更方便的调用也让他们更好的了解其使用方法或者意思,那好的办法是选择简洁的名称,并为其写上足够说明其意思和使用方法的注释。

Getters

Go 不会自动的提供 getters 和 setters 函数,但是这种事情你完全可以自己去完成,但是你也应该在适当的时候这么做,但是,你不需要把 Get 写进你所定义的方法名称中去,比如你现在有一个字段 owner(注意这是小写字母开头的,所以不导出,外部也部无法直接调用),它的 getter 名称应该称之为 Owner而不是GetOwner,它成为了获取 owner 字段数据的钩子,而 setter 函数则应该称之为 SetOwner:

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}

接口名称

Go 约定接口名称为方法名称后面加上 er:Reader、Writer、Formatter等,Go中有许多这样名称的标准接口,也有很多方法去实现了这些接口,它们这些方法的名称都是像 Read、Write、Close、Flush、String等这样的,为了避免造成混乱,在你自己的程序中尽可能的不要去使用上面这些标准库中提供了的方法或者接口名称。

相反的,如果你的程序实现了一个与某一个已经被另一个大家所熟知的程序实现过的接口,那么你最好是使用该程序所使用的方法名称,一个将你的数据转换为字符的方法,不要叫作ToString,而应该直接叫作 String。

多词汇名称

最后,Go约定使用“驼峰式”的命名,而不使用下划线的方式,比如MixedCaps或者mixedCaps而不是mixed_caps。

分号

与 C 一样,Go使用分号“;”来终止一条语句,而与C不一样的是这些分号一般是不会出现在的源代码中的,Go使用一种简单的规则在其扫描代码的过程中合适的插入这些分号,所以,你在写代码的时候完全不需要将其明显的写入进源代码中。

Go所约定的规则是这样的:如果一个新行的最后面是一个类型识别码(一般都是 int 或者 float64这样的),一个常量(比如数字或者字符串)或者下面这些关键字或者符号中的任何一个:

break continue fallthrough return ++ -- ) }

Go 会自动在其后面加了分号,上面所说的这些可以简单的描述为:“如果一个新行前面的Token可以终止一个声明,则在该Token后插入分号”。

结束大括号前的分号也是可以省略的,比如:

go func() { for { dst <- <-src } }()

就不需要分号,Go程序只需要在诸如循环、分隔初始值、条件或者延续元素的情况,才需要明显地使用分号,当然,你可以将多行地声明写进同一行中,这个时候每一个声明后就需要使用分号来区分。

有一点你需要特别注意的,你不应该把控制结构(if、for、switch或者select)的左大括号写在下一行,如果你这么做了,那么Go会在这些声明后面加上一个分号,这肯定不是你想看到的。

if i < f() {
    g()
}

而下面这样就是错误的:

if i < f()  // wrong!
{
    go ()   // wrong!
}

控制结构

Go 的控制结构与 C 十分类似,但是有一个重要的不同在于Go没有 do 或者 while 循环,仅仅使用 for 来做这些基本工作,switch 有更多的灵活性以及if和switch可以接受一个初始值声明,它还有一个新的控制结构 select,语法也略有不同:没有括号,主体必须分隔。

if

在Go中一个简单的if语句看起来像下面这样的:

if x > 0 {
    return y
}

强制性的要求 if 声明必须被分隔为多行,由于是在它的主体中包含有如 return或者break等控制语句的情况更为适用。

由于 if 和 switch 支持初始化声明,所以,我们可以在其条件判断前为该条 if 语句初始化一个本地变量:

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

在Go标准库中,如果一个 if 语句不接会导致程序继续执行其下一条语句时,我们可以不需要使用 else ,这通常的都是在if主体中存在 return、break、continue或者goto等等这些能控制代码结构的关键字。

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.close()
    return err
}
codeUsing(f)

在上面的代码中,我们首先打开一个文件,如果没有错误存在,程序会继续往下执行,如果再没有错误,则会执行 codeUsing(f)函数,但是如果有错误出现,那么在if执行之后就会跳出整个程序。

声明重载(Redeclaration)

在上面的示例中,你应该看到,我们的一个操作符 := 将 os.Open 返回的值同时声明给了两人个变量:f和err:

f, err := os.Open(name)

接着:

d, err := f.Stat()

看到,err 在前面声明之后,我们接着又重新声明了它,这表明,在第一个声明中,创建了err并为其赋了值,而在后面一次声明中,仅仅只是为其赋了新值。

在一个为变量 v 赋值的 := 的声明中,那么:

  • 如果在该声明同样的作用域内该声明之前已经存在了对该变量的声明,那么本声明直接为其赋值
  • 没有出现上面的情况,本声明将先创建该变量之后再为其赋值

for

Go的for循环有点像C的for循环——但是是不一样的,它有三种不同的格式:

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

更短的声明让我们更容易给循环一个索引变量:

sum := 0
for i := o; i < 10; i+ {
    sum += 1
}

如果你在遍历一个数组、片(Slice)、字符串或者Map,或者从一个 channel 中读取数据,range 可以被用来控制该循环:

for key, value := range oldMap {
    newMap[key] = value
}

如果你仅仅只需要循环过程中 range 产生的第一个值(range 产生两人个值 key, value),那么直接丢掉第二值即可:

for key := range m {
    if expired(k) {
        delete(m, key)
    }
}

但是如果你仅仅只想要其产生的第二个值,那么需要使用一个“空定义(blank identifier)”作为占位符,它是一个下划线:“_”:

sum := 0
for _, value := range array {
    sum += value
}

这是因为,如果你没有那个占位符,那么 range 还是会首先把它产生的第一个值赋值给 value,而有了那个点位符之后,range将key赋值给,之后会直接把该值丢掉,然后range就赋值其第二值 value给value了。

对于字符串,range能做更多的事情,breaking out individual Unicode characters by parsing the UTF-8. Erroneous encodings consume one byte and produce the replacement rune U+FFFD. The loop

func main() {
        for pos, char := range "编程开发" {
                fmt.Printf(" aracter %c starts at byte position %dn", char, pos)
        }
}

将会打印出:

aracter 编 starts at byte position 0
aracter 程 starts at byte position 3
aracter 开 starts at byte position 6
aracter 发 starts at byte position 9

最后,Go没有逗号操作符,而 ++ 与 -- 是声明而不是表达式,如果你想在一个for循环中使用多个变量,你需要使用 Parallel Assignment。

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

switch

Go 中的 switch 比C中的更加通用,它的表达式并不局限在常量或者数字中,它的 case 是从上到下进行匹配,直到找到一个为 true 的 case,如果你的 if-else-if-else-... 有点儿太多了,那么就使用 switch吧。

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
`   return 0

Go 中没有自动向下执行(Fall Through)机制,所以,你不能写成这个样子:

case i < 0:
case i > 10:
    doSomethingWithI(i)

但是Go可以将多个条件使用逗号分开,所以上面这样在其它语言里面常见的写法在Go你应该写成:

case i < 0, i > 10:
    doSomethingWithI(i)

Switch还可以被用来发现某一个接口变量的动态类型,像下面这样的去实现:

switch t := interfaceValue.(type) {
default:
    fmt.Printf("unexpected type %T", t)
case bool:
    fmt.Printf("boolean %tn", t)
case int:
    fmt.Printf("integer %dn", t)
case *bool:
    fmt.Printf("pointer to boolean %tn", *t)
case *int:
    fmt.Printf("pointer to integer %dn", *t)
}

函数(Functions)

多值返回 (Multiple return values)

Go函数和其它语言最不一样的特性就是它可以在一个函数中同时返回多个值,这个特性能改进很多在C里面要通过各种各样的特殊办法实现的功能,而这个特性你在本文档前面也已经见到过了:

f, err := os.Open(name)

同样的,在os包中还定义的很多比如 Write方法:

func (file *File) Write(b []byte) (n int, err error)(

就像它的文档所说的,该方法返回写入的总字节数,并且当 n != len(b) 的时候返回一个 non-nil 的 error,这种写法在Go很通用,你可以在专门的章节中看到更多的关于 error 处理的示例。

类似的这种方法省却了一个指针的使用,而且它看起来更加符合我们的普通常人的理解(需要返回什么东西就直接返回就是),下面这个示例演示了一个能同时返回两个值的函数,它先从一个字节数组(byte array)中找到一个数字,然后返回下一个为数字的元素的位置:

func nextInt(b [byte], i int) (int, int) {
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

然后你可以像下面这样的去调用它:

for i := 0; i < len(a); {
    x, i = nextInt(a, i)
    fmt.Println(x)
}

带名称的结果参数

Go 函数中的返回结果参数可以被赋予一个名称,它可以像其它的变量一样的在函数中使用,命名之后,当函数执行时,它会被赋予一个其所属的数据类型的0值,而当函数执行一个 return声明之后(不需要带任何参数的声明),该命名了的返回参数当前的值就会被返回。

Go 不强制你必须为返回参数命名,但是这样做可以使你的代码更短——它们被文档化了。如果我们将上面的函数进行命名,那么:

func nextInt(b []byte, pos int) ( value, nextPos int) { ... }

因为命名了的返回结果会在函数执行时被初始化了,并且和一个未经修饰的 return 绑定,所以它能使唤代码更清晰,下面这是一个 io.ReadFull的一个版本:

func ReadFull(r Reader, buf []byte) ( n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

延迟(Defer)

Go的defer声明一个定时运行的函数呼叫(deferred function),这样的函数会在函数终止前立马执行,这种方式在其它方法里面不太常见,但是它却非常有用,比如我们有一个函数要打开一个文件,但是不管这个函数是否执行成功,我们都应该在不再需要这个文件的时候关闭这个文件,这个时候我们就可以在成功打开文件之后,立马定时关闭该文件,当函数再往下执行时,不管是出现什么错误或者是成功执行,文件都会被正确关闭。

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.close() // f.close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err // f will be closed if we return here
        }
    }
    return string(result), nil // f will be closed if we return here
}

延迟请求一个函数的执行至少有两人个好处:首先,它能让你永远不会因为某些错误(这实在是太容易发生了)而关闭文件;其次,关闭与打开文件的代码可以被写在一起,这样后面的代码完全可以放开安排自己的代码结构了(肯定啊,不需要在每一个 return 前面关闭一次文件了,我在任何一个地方可以 return ,文件会自动的关闭)。

传递给被延迟请求的函数的参数是在 defer 执行是就计算出来的,而不是函数执行时才计算的,所以像下面这段代码运行的结果将是 10 0,而不是10 10:

func forDefer() {
    i := 0
    defer fmt.Printf("%d ", i)
    i = 10
    fmt.Printf("%d ", i)
}

同时它还遵循LIFO(Last Input First Output:先进后出)所以,当defer执行之后,延迟函数所调用的参数不管再怎么改变,都不会影响延迟函数本身的输出,所以像下面这段你代码,它最终执行后的结果将不是 0 1 2 3 4,而是4 3 2 1 0:

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

延迟函数执行还时常被用在跟踪函数本身的执行上,比如下面的示例:

func trace(s string)    { fmt.Println("entering: ", s) }
func untrace(s string)  { fmt.Println("leaving: ", s) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something you like as normal...
}

对于这个跟踪函数,我们还可以做得更简洁高效,还记得我们在前面了解到的关于延迟函数的参数的值是在defer执行时就被计算的吗?我们在这里来使用这一特性重新完成前面的那个函数运行跟踪函数:

func trace(s string) string{
    fmt.Println("entering: ", s)
    return s
}

func un(s string) {
    fmt.Println("leaving: ", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    def un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

上面代码的输出结果是:

entering: b
in b
entering: a
in a
leaving: a
leaving: b

对于习惯于以块为单位管理代码的其它语言的开发者来说,defer可以看起来很奇怪,但是它功能的功能及实用性是可以肯定的,而且是十分有趣的,它使得代码不在以块为单位而是以功能来进行组织管理的。

数据

通过 new 分配内存

Go内置了两人函数 make 和 new,它们做的事情是不一样的,而且绑定了不同的类型,这很容易让人混淆,但实际上它的规则是很简单的。让我们先来谈谈 new,它是一个用来分配内存的内置函数,但是它不像其它语言中的 new 函数,它不初始化内存, new(T) 只是创建一个没有任何数据的类型为T的实例,并且返回它的地址——*T类型的值,在Go的术语中,它返回一个新建的分配了0空间的类型为T的实例的指针。

由于new返回的内存是归零了的,所以它能让我们很方便的设计它的数据结构,因为归零的数据可以不需要更多的初始货即可直接使用,比如 bytes.Buffer的文档这么写着“Buffer的归零数据是一个空的可以随便人到中年来使用的缓冲”。相反的,sync.Mutex没有明确的构建或者初始化方法,转而它的归零值为一个未锁定的mutex。

The zero-value-is-useful property works transitively. Consider this type declaration.

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

SyncedBuffer类型的值在其分配了或者仅仅只是声明了之后都是可以仍的,下面这段代码不需要更多的安排即可以正确的工作:

p := new(SyncedBuffer)  // type *Synceduffer
var v SyncedBuffer  // type SyncedBuffer

构造函数与复合声明(Constructors and composite literals)

很多时候一个归零的值并不足以满足程序的业务需求,这时一个初始化构造函数就成了必须的东西了,比如下面这段从 os 衍生的函数:

func NewFile(fd ind, name string) *File{
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

上面这样的写法有点太过于复杂和麻烦了,使用初始化成员可以更简单的完成这个工作,每次只需要使用一个表达式即可创建一个新实例:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File(fd, name, nil, 0}
    return &f
}
注意:不像 C,返回本地变量的地址在Go中是完全可以的,当函数返回之后,本地变量地址所占用的存储空间同样的会生存下来,事实上,我们可以直接可以将上面函数中的最后两人行合并为一行:
return &File(fd, name, nil, 0}

上面这些初始化成员必须按其构造函数定义了的顺序赋值,这是很麻烦的一件事情,我们必须要一直记得它们是一个什么样的顺序,即使有100个,我们也必须记住这100个的顺序,好在我们可以使用一种更加合理且方便的方法来使用它,那就是为每一个初始化成员指定一个标签,其格式为field: value:

return &File(fd: fd, name: name}

如果初始化成员没有任何元素的话,那么就会创建一个归零的实例,这个时候,&File{}与new(File)是等价的。

复合声明同样可以被用来创建array、slice、map,通过指定适当的索引和map键来标识字段,在下面这个例子中,无论是Enone、Eio还是Einval初始化都能很好的工作,只要确保他们不同就好了。

a := [...]string    {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string       {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}

用 make 分配内存

回到内存分配。内奸函数 make(T, args) 与 new(T) 有着不同的功能。它只能创建 slice、map和channel,并且返回一个有初始值(非零)的T类型的实例,而不是 *T。从本质上讲,导致这三个类型有所不同的原因在于指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量这三项的描述符,在这些项目被初始化之前,slice为nil,对于slice、map和channel,make初始化了内部的数据结构,填充适应的值。

make 返回初始化后的(非零)值

例如,*make([]int, 10, 100)分配了100个整数的数组,然后用长度10和容易100创建了slice结构指向数组的前10个元素,区别是new([]int)返回指向新分配的内存的指针,而零值填充的slice鸨是指向nil的slice值。

下面这个例子展示这两人者之间的不同:

var p *[]int = new([]int)   // 分配slice结构内存 : *p = nil
                // 已经可用
var v []int = make([]int, 100)  // v 指向一个新分配的有 100 个整数的数组

var p *[]int = new([]int)   // 不必要的复杂的例子
*p = make([]int, 10, 100)

v := make([]int, 100)       // 更常见

务必赢得 make 仅适用于 map、slice和channel,并且返回的不是指针,应当用new来获得特定的指针。

本文是 「Golang.org」的原文档翻译,出于本人有限的英语水平,如有不当,还请指正。

说明

本文档演示了一个最简单的 Go 包的开发过程以及一些 Go 命令行工具的使用,以及Go是如何架构、安装与搜索包的。

代码组织

GOPATH 以及工作区

Go 语言设计的目标之一是让软件开发更简单,得到的结果是 Go命令不使用任何的 Makefiles或者其它的配置文件,取而代之的是直接使用程序的源代码来管理依赖关系及架构条件,这意味着,你程序的源代码与架构脚本总是同步的,因为它们就是同样的东西(注:如果使用 Makefiles之类的东西,那么,你源代码修改了,还需要再去修改Makefiles,而是在Go中,因为没有这些配置文件,所以,你修改源代码,同时直接就修改了这些配置)。

你唯一需要做的是设置一个 GOPATH 环境变量,GOPATH 的作用是告诉 Go 从哪里去搜索Go的执行脚本以及将你的Go安装到哪里.$GOPATH 是一个包括很多路径的列表,它的格式与系统的PATH变量格式一样,一个Unix系统中常见的GOPATH像这样的:

GOPATH=/home/user/ext:/home/user/mygo

(在Windows系统中,将“:”修改为“;”)

任何一个被定义至 $GOPATH$ 环境变量中的路径都指定着一个工作区的的位置(比如上例中的 /home/user/ext或者/home/user/mygo),一个工作区包含着Go源代码以及它们所依赖的包对象以及一些可执行的工具,它被规定为需要有下面这三个子目录:

  1. src : Go 源代码文件
  2. pkg : 以编译的 Go 包
  3. bin : 可执行命令

src 目录中的子目录都是独立的包,这些包中的所有源代码文件(.go,.co,.h以及.s等)都是子目录包的元素。

它构建一个import "widget"的程序时,Go构建工具会搜索 Go 根目录下的 src/pkg/widget 目录——如果没有找到,那么它会接着尝试去寻找同一工作区中的 src/widget 目录。

多个工作区可以给我们带来更我铁灵活性与方便,但是在本文档中,我们只涉及单个工作区,下面让我们先创建一个示例工作区,首先创建一个目录,我们用它来存放项目的源代码:

cox@CoxStation:~$ mkdir $HOME/example/src -p

下一步,设置 GOPATH 环境变量,你需要同时将 $GOPATH/bin 添加到 PATH 环境变量中,这样一来,你就不需要每次执行这些命令的时候输入完整的绝对路径了。

cox@CoxStation:~$ export GOPATH=$HOME/example
cox@CoxStation:~$ export PATH=$PATH:$HOME/example/bin

导入路径

Go 标准库或者包都可以使用最短的路径名导入,比如 fmt 或者 net/http 等,这是为了程序开发者的方便而设计的,但是对于你自己的项目,你需要认真的设计一个基本的导入路径,这需要考虑它它的可读性同时还需要考虑到它不应该和以有的或者将来会出现的其它的库相冲突。

最好的一个办法是总是以你所开发的项目的版本管理工具名称开始,比如,如果你的项目源代码库位置为 example.com 或者 code.google.com/p/example ,那么你应该让你的包路径从该 URL 地址开始,比如:example.com/foo/bar 或者 code.google.com/p/example/foo/bar ,如果你遵循这样的约定,那么 Go 会自动为你检测URL地址所指定的包,同时下载以及以你的导入路径安装至你的工作区中。

如果你没有像上面这样的发布路径,那么你至少应该使用一个唯一的前缀,比如 widget 以及 widget/foo/bar ,一个好的规则是使用你的项目名称或者公司名称作为前缀,这一般是不会被别人使用的,比如:example.com。

我们下面使用 example.com 作为基本的导入路径:

cox@CoxStation:~$ mkdir -p $GOPATH/src/example.com

包名称

任何一个 Go 源代码文件的第一个声明都应该是包名:

package NAME

NAME 就是你的包名称,它也是你导入该包时默认使用的名称(所有处于同一个包中的文件都必须共享同一个包名)。

Go 约定包名称是导入路径中的最后一个元素,一个以 crypto/rot12 导入的包的包名应该是 rot13,Go 不要求所有被导入到某一个程序中的包名都具有唯一性,但是要求其完整的导入路径必须是唯一的。

在 example 中创建一个新包,名为 newmath:

cox@CoxStation:~$ cd $GOPATH/src/example.com
cox@CoxStation:~/example/src/example.com$ mkdir newmath

之后创建一个名为 $GOPATH/src/example.com/newmath/sqrt.go 的文件,它的内容为:

// Package newmath is a trivial example package.
package newmath

// Sqrt returns an approximation to the square root of x.
func Sqrt(x float64) float64 {
    // This is a terrible implementation.
    // Real code should import "math" and use math.Sqrt.
    z := 0.0
    for i := 0; i < 1000; i++ {
        z -= (z*z - x) / (2 * x)
    }
    return z
}

该包将可以使用下面这个路径导入:

import "example.com/newmath"

构建以及安装

Go 命令包含了很多子命令,最核心的子命令就是install,运行go install importpath 将构建并安装它所依赖的所有包。

安装一个包 表示将构建之后的包文件或者可执行的文件写入到当前工作区的pkg或者bin目录中。

构建一个包

要构建并安装 newmath包,使用下面命令:

cox@CoxStation:~/example$ go install example.com/newmath

如果构建成功,那么该命令不会有任何输出。Go约定,如果没有指定导入路径,那么go使用当前目录,下面的这两行命令与上面那个是一样的效果:

$ cd $GOPATH/src/example.com/newmath
$ go install

构建之后的结果是go为你创建了一个目录树(假定我们现在使用的是Linux 64位系统——我就是这样的系统),它看起来像是这样的:

$GOPATH/
    pkg/
        linux_amd64/
            example.com/
                newmath.a
    src/
        example.com/
            newmath/
                sqrt.go

构建一个命令工具

Go 命令将包名为 main 的包视为可执行的,从而会将它安装到 $GOPATH/bin 目录中,创建一个名为 hello 的可执行命令,我们首先需要创建它的包,之后再创建包文件:

cox@CoxStation:~/example$ mkdir $GOPATH/src/example.com/hello
cox@CoxStation:~/example$ vi $GOPATH/src/example.com/hello/hello.go

文件内容为:

// Hello is a trivial example of a main package.
package main

import (
    "example.com/newmath"
    "fmt"
)

func main() {
    fmt.Printf("Hello, Golang. Sqrt(2) = %vn", newmath.Sqrt(2))
}

保存上面文件之后,我们运行下面这个命令安装它:

cox@CoxStation:~/example$ go install example.com/hello

之后你就可以像运行其它命令一样的运行我们的 hello 了:

cox@CoxStation:~/example$ $GOPATH/bin/hello
Hello, Golang. Sqrt(2) = 1.414213562373095

如果你将 $GOPATH/bin 目录添加到了全局环境变量 PATH 中的话,那么你还可以像下面这样的执行:

cox@CoxStation:~/example$ export PATH=$PATH:$HOME/example/bin/
cox@CoxStation:~/example$ hello
Hello, Golang. Sqrt(2) = 1.414213562373095

现在我们的示例程序的目录数看起来应该像下面这个样子的了:

$GOPATH/
    bin/
        hello
    pkg/
        linux_amd64/
            example.com/
                newmath.a
    src/
        example.com/
            hello/
                hello.go
            newmath/
                sqrt.go

测试

Go 内置了一个轻量级的测试框架,它由 go test 命令和 testing 包组成。

创建一个以 _test.go 结尾的文件,让它包含只有用来标记的参数 t *testing.T的名为TestFUNC* 的函数,那么测试框架就会执行每一个像上面这样的函数,其实函数名称中的 FUNC 修改成为你需要测试的函数名,如果该函数调用了失败函数,比如t.Error或者t.Fail,那么表示整个测试失败。

我们现在创建一个名为 $GOPATH/src/example.com/newmath/sqrt_test.go 的测试文件用来测试 Sqrt函数:

cox@CoxStation:~/example$ vi $GOPATH/src/example.com/newmath/sqrt_test.go

它的内容为:

package newmath

import "testing"

func TestSqrt(t *testing.T) {
    const in, out = 4, 2
    if x := Sqrt(in); x != out {
        t.Errorf("Sqrt(%v) = %v, want %v", in, x, out)
    }
}

现在运行测试:

cox@CoxStation:~/example$ go test example.com/newmath
ok      example.com/newmath 0.005s

远程包

一个导入路径可以告诉 go 如何从一个版本管理系统(比如Git或者Mercurial)中获取包代码,go命令使用这些属性自动从远程搜索包,比如我们的这个示例程序,它的远程地址是:code.google.com/p/go.example,如果你将该URL址址告诉go,那么go会自动从远程下载该包的源代码:

$ go get code.google.com/p/go.example/hello
$ $GOPATH/bin/hello
Hello, world. Sqrt(2) = 1.414213562373095

如果指定的远程包在本地没有复本,那么 go 会自动的为你下载它,如果已经存在,则直接跳过,运行了上面的命令之后,你的目录看起来应该像下面这样的了。

$GOPATH/
    bin/
        hello
    pkg/
        linux_amd64/
            code.google.com/p/go.example/
                newmath.a
            example/
                newmath.a
    src/
        code.google.com/p/go.example/
            hello/
                hello.go
            newmath/
                sqrt.go
                sqrt_test.go
        example/
            hello/
                hello.go
            newmath/
                sqrt.go
                sqrt_test.go

如果你不直接告诉 go 去下载安装某个包,而是在某一个你所写的包中导入它,比如:

import "code.google.com/p/go.example/newmath"

那么,go 一样的也会自动的为你做好这一切。

说明

  1. 本翻译文档中所涉及的代码和原始文档有些话差入。
  2. 你可以从这里下载到本文档中所创建的示例代码:http://dl.antusoft.com/examples/golang/go.example.tar.gz
  3. 官方示例项目地址为:http://code.google.com/p/go.example/

什么是Go?来自其网站的介绍:

Go 编程语言是一个使得程序员更加有效率的开源项目。Go 是有表 达力、简洁、清晰和有效率的。它的并行机制使其很容易编写多核 和网络应用,而新奇的类型系统允许构建有弹性的模块化程序。Go 编译到机器码非常快速,同时具有便利的垃圾回收和强大的运行 时反射。它是快速的、静态类型编译语言,但是感觉上是动态类型 的,解释型语言。Go 1 是 Go 语言的第一个稳定发布版本。

基本上任何一个语言最开始的学习的时候都离不开那个“Hello World!”程序,既然找不到其它的方法,那么,学习Go的时候还是也来一个Hello World吧。

package main

import "fmt"

/* 打印出 Hello World 的各种语言的版本 */

func main() {
    fmt.Printf("Hello, world; or 你好,世界n")
}

在上面的代码段中,首行是必须的,任何 Go 文件都必须以 package 开头,对于需要独立运行的执行文件,则必须是 package main;接着我们导入了 fmt 包,这就是说需要将 "fmt" 包加入 main,不是 main 的其它包都称之为库,其它许多语言中都有着类似的概念,比如Python中的 module。后面跟着的是一行注释。

最后面我们定义了一个名为 main() 的函数,当 Go程序在执行的时候,首先调用的函数就是 main.main(),这是从C语言中继承而来的,在这里我们使用关键字 func 定义了这个函数,这个函数不接受任何参数,也不返回任何类型的值,该函数将打印出 “Hello, world; or 你好,世界”字符串,你可以发现,在Go语言中,可以直接使用非ASCII字符,这是因为Go支持任何编码的字符串,你甚至可以使用非ASCII编码的字符作为变量或者函数名称。

将上面的代码保存为一个helloworld.go 文件,然后编辑它:

go build helloworld.go

这会在当前文件夹中生成一个同名的可执行文件,我现在使用的是Windows 7,所以,这个可执行文件的文件名为 helloworld.exe 。如果不想编译它即执行,可以使用 go run 命令:

go run helloworld.go

它会直接运行 helloworld 程序,而不是生成可执行文件后手工去执行。

直接进入正题,在任何一个地方创建一个名为 helloworld.go 的文件,然后立马执行它:

cox@Cox:~/workspace/go/src$ touch helloworld.go
cox@Cox:~/workspace/go/src$ go run helloworld.go
package :
helloworld.go:1:1: expected 'package', found 'EOF'

简单吧,看错误信息,你现在应该知道,没有绑定到某一个 package 的文件是不被 Go 认可的,所以,我们都需要加上这么一个,我们在前面的那个 helloworld.go 文件里面加上:

package main

之后再来执行:

cox@Cox:~/workspace/go/src$ vi helloworld.go
cox@Cox:~/workspace/go/src$ go run helloworld.go
# command-line-arguments
runtime.main: undefined: main.main

恩,Go程序需要从某一个位置开始执行,就像电脑有电源开关一样简单,在 Go 中,程序都是从一个名为 main 的函数开始执行,所以,再加上这个函数:

package main

func main() {}

再来执行它:

cox@Cox:~/workspace/go/src$ vi helloworld.go
cox@Cox:~/workspace/go/src$ go run helloworld.go

恩,没报任何错误了,那也就是说我们现在的程序已经正确运行,但是因为 main() 没有做任何事情,所以我们现在什么也看不到,我们现在让它来打印出 “Hello World! 您好!世界。” 这个字符串:

package main

func main() {
    Println("Hello World! 您好!世界。")
}

又出错了:

# command-line-arguments
./helloworld.go:4: undefined: Println

Println 未定义,恩,从我们上面的代码中确实看得到,从来没有它,突然就这么一下子就拿来用了,不成,就像你从来没有买过车,一辈子都没见过车,怎么可能走着走着就突然在开车了呢,我们来买一辆车:

package main

import "fmt"

func main() {
    fmt.Println("Hello World! 您好!世界。")
}

再来运行 helloworld.go:

cox@Cox:~/workspace/go/src$ go run helloworld.go
Hello World! 您好!世界。

成了,这个 fmt 就是我们买来的车,有了车我们才能开车,我们这里用了 fmt 里面的 Println 函数。

你学会了吗?

  • 任何一个 Go 文件都必须被关联到某一个包上
  • 任何一个 Go 程序都必须有一个开始点,就是那个 main 函数
  • 任何一个函数或者变量都必须被定义之后才能使用

Golang 是一个以类BSD许可发布的程序开发语言,官方提供有两个编译器 gc Go编译器和gccgo编译器,其中gccgo编译器是GNU C 编译器的一部分,而 gc 编译器比较成熟并且它进行了更多的测试,本文所使用的是 gc 编译器安装一个以二进制包发布的 Go 版本。

系统需求

gc 编译器可以在下面这些操作系统和架构上面运行,请确定您当前的系统满足这些需求,如果你所使用的系统不在当前列表中,那么你就应该试试 gcc 编译器安装,这有可能有适合您的系统。

操作系统名称 系统架构 说明
FreeBSD 7+ amd64,386 Debian GNU/kFreeBSD不被支持
Linux 2.6.23+(glibc) amd64,386,arm CentOS/RHEL 5.x不被支持,还无ARM版本的二进制包
Mac OS X 10.6/10.7 amd64,386 使用XCode的gcc(1)
Windows 2000+ amd64,386 使用mingw gcc(2),cygwin或msys不是必需的

1) 只有你想使用 cgo 时,gcc才是必须的;2) 你仅仅只需要安装命令行工具即可,如果你已经嬢了  Xcode 4.3+,那么你可以在组件面板中添加命令行工具

下载 Go Tools

访问 Go 项目下载页面:http://code.google.com/p/go/downloads/list,选择一个适合你的操作系统或者CPU架构的发行版,官方的发行版有FreeBSD,Linux,Mac OS X(Snow Leopard/Lion)以及Windows系统,同时支持 32位与64位CPU架构,如果没有适合你所使用的操作系统的二进制安装包,你可以需要从源代码安装或者使用gccgo而不是gc。

安装 Go Tools

Go的发行布将被安装至 /usr/local/go目录(Windows下为c:Go),但是你是可以修改该安装目录的,使用Go时,你需要指定 GOROOT 环境变量至你使用 Go 的目录,比如,如果你将 Go 安装至你的home目录,那么你需要在 $HOME/.profile文件中添加下面这两行:

export GOROOT=$HOME/go
export PATH=$PATH:$GOROOT/bin

Windows用户请设置环境变量,怎么设置我也不太明白,很少用那个东西……

FreeBSD, Linux以及Mac OS X压缩包

如果你是从一个老版本的Go进升级,那么你需要在安装之前先删除原先版本:

rm -r /usr/local/go

将你获得的压缩包文件解压至 /usr/local:

tar -C /usr/local -xvf go1.0.3.linux-amd64.tar.gz

包文件的名称可能和上面我所使用的不一样,因为我们的系统可能不一样,之后你可以将 /usr/local/go/bin目录添加至你的环境变更 PATH 中,比如添加至 /etc/profile或者$HOME/.profile。

Mac OS X 包安装工具

如果 Mac OS X 的包安装工具的话只需要接照安装工具的流程进行即可,默认会安装至 /usr/local/go 目录中。

Windows 下面自己研究

测试你的安装

要测试你的Go是否已经正确安装的最简单的办法就是写一个Go程序,比如,Hello Golang

package main

import "fmt"

func main() {
    fmt.Printf("Hello, Golang!n")
}

然后测试运行:

$ go run hello.go
Hello, Golang!

OK,这表示你安装成功了。