去年偶然之间接触到Golang,接触到effective_go这篇文章。
其中对于同步的另辟蹊径,让当时的我激动了好久,也自以为理解了《奇思妙想》中关于兰伯特时钟的讨论。
在不需要同步时,每个任务都有自己的时钟,仅在需要同步时同步时钟。
这在Golang中就像“通过通信来共享”,表现出来就是使用channel对消息进行阻塞。
另外,其中”channel of channel”中对于request的封装模型也让人印象深刻。遗憾的就是目前我还未在实际编程中应用这个模型。
当时一口气读完之后,觉得大快人心,如此好文不论是Golang或是其它语言都有一定的启发性,于是萌生了翻译该文的想法。
但原文写得实在精彩,限于本人能力有限,对文章的理解与遣词造句不尽合理之处欢迎大家批评指正。
(这篇文章老早翻译了,原文链接稍后补上)
简介
Go 是一门新生的语言。尽管它从其他现存语言中借鉴了许多理念,但其与众不同的特性,
使得使用 Go 语言编程在本质上与其它语言大为不同。将现有 C++ 或 Java 的程序直接转
换到 Go 程序总是不尽如人意 — Java 程序是用 Java 写的,而不是 Go。
另一方面,从 Go 的角度分析问题能够编写出一个同样可行但大不相同的程序。
换句话说,理解 Go 的特性以及惯用法对于编写好的 Go 程序极为重要。
熟悉 Go 编程中约定俗成的规范如命名、格式化、程序结构等同样重要,
这将使得你写出的程序对于其它 Go 程序员更易理解。
这份文档将对如何写出简洁、地道的 Go 程序给出一些建议。它对
语言规范 ,
Go 语言入门 ,
和 如何使用 Go 编程 作了更进一步的阐述,我们建议你先阅读这些文档。
范例
Go 源码包 不仅仅作为核心实现库,你可以将它看作学习 Go 语言的很好的例子。
如果你有任何关于某个问题如何解决或某些方法如何实现的疑问,它都将给你一些答案、思路以及后台实现。
格式化
格式化的讨论是最具争论性的,但始终没有统一的定论。人们能够适应不同的编码风格,但如果不需要适应
各种风格岂非更妙?而且如果大家遵循同样的编码风格,人们就能避免在这个话题上浪费更多的时间。
然而问题在于如何在丢弃一份“又长又臭”的语言风格规范手册的同时达到这个“乌托邦”。
在 Go 语言中我们另辟蹊径,让机器去处理大部分的格式化问题。gofmt
程序
(或者用 go fmt
,它以包为处理对象而非源文件)将 Go 程序按照标准风格缩进、对齐、保留
以及在必要时候重新格式化输出注释。
如果你想知道如何处理某些新的代码布局情况,尝试执行 gofmt
;如果结果不尽人意,
重新组织你的程序(或者提交关于 gofmt
的bug),而不是纠结于此。
例如,你没必要花时间对齐结构体字段的注释, Go fmt
将为你代劳。假设你有如下声明
type T struct { name string // name of the object value int // its value }
type T struct { name string // 对象名 value int // 对象值 }
gofmt
会对齐这些列:
type T struct { name string // 对象名 value int // 对象值 }
标准包中的所有 Go 代码都已使用 gofmt
格式化。
还有一些关于格式化的细节。非常简短,
- 缩进
- 我们使用制表符tab进行缩进,
gofmt
默认也使用tab。
在你认为确实有必要时才使用空格。 - 行的长度
- Go 对行的长度没有限制。别担心打孔纸不够长。
如果某一行看起来过长了,进行适当折行并插入tab缩进。 - 括号
- Go 很少使用括弧:在结构控制(
if
,for
,switch
)
的语法中并不强制使用括号。
同样的,操作符优先级处理更加简洁。因此x<<8 + y<<16
正表述了空格符所传达的含义。
注释
Go 语言支持 C 风格的 /* */
块注释以及 C++ 风格的 //
行注释。
行注释较为通用,块注释通常在包注释以及大块代码注释的情况下使用。
godoc
程序——或可理解web服务器——从 Go 的源文件提取出关于包内容的说明文档。
出现在顶层声明的无空行间隔的注释与其对象声明将被提取为对该对象的说明文档。
这些注释的类型以及风格决定了 godoc
产生的文档质量。
每一个包都应包含一段包注释,即一段在包内容之前的块注释。
对于具有多个文件的包,包注释仅需要出现在其中一个文件中,该包的任一文件均可。
包注释需包含包的介绍以及该包的相关信息。
它将首先出现在 godoc
页,并紧随着更详细的文档说明。
/* regexp 包包含正则表达式的一些简单实现库。 正则表达式的语法包括如下: regexp: concatenation { '|' concatenation } concatenation: { closure } closure: term [ '*' | '+' | '?' ] term: '^' '$' '.' character '[' [ '^' ] character-ranges ']' '(' regexp ')' */ package regexp
如果某个包比较简单,包注释同样可以简洁些。
// path 包实现了以斜线分隔的文件路径的有效处理例程。
注释不需要额外的诸如星号”*”的格式。产生的输出甚至不会以定宽字体显示,因此不要为了对齐而插入额外的
空格— godoc
会像 gofmt
那样处理好这一切。
注释为不可解释的纯文本,因此HTML以及其他注释,如 _this_
,会被逐字的翻译,应避免使用此类评注。
godoc
是否重新格式化注释取决于上下文,因此确保它们看起来清晰:
使用正确的拼写、标点、语法结构以及压缩较长的行等。
在一个包中,任何顶层声明的注释都将被提炼为该声明的说明文档。
程序中所有可导出的实例名(首字母为大写)都应有对应的说明文档。
说明文档最好是完整的句子,这样它才能适应各种自动化输出。
第一句注释应为以被声明的对象名字起始的单句概述。
// Compile 方法解析一串正则表达式,解析成功将返回一个能够被用于匹配文本的Regexp对象。 func Compile(str string) (regexp *Regexp, err error) { ... }
Go 的声明语法允许组合声明,说明文档应包含对一组相关的常量或变量的介绍。
因为要解释所有的声明,这样的注释通常较为笼统。
// 解析表达式出错时返回的错误码。 var ( ErrInternal = errors.New("regexp: internal error") ErrUnmatchedLpar = errors.New("regexp: unmatched '('") ErrUnmatchedRpar = errors.New("regexp: unmatched ')'") ... )
即使是私有名称,组声明也能够表明各项之间的关系,
例如某一组变量由某个互斥锁保护。
var ( countLock sync.Mutex inputCount uint32 outputCount uint32 errorCount uint32 )
命名
正如命名在其它语言中的地位,它在 Go 中同样重要。
有时它们甚至能够产生语义效应:例如,某个名称在包外是否可见取决于它的第一个字
母是否是大写。
因此,我们十分有必要花点时间讨论 Go 中的命名规范。
包命名
当某个包被导入时,包名成为了包内容的访问器。在
import "bytes"
之后,该被导入的包可以这样引用 bytes.Buffer
。如果所有人都能够
使用相同的名字引用包内容将大有裨益,这也意味着包应该有个恰当的名称:
简短、明确、具有启发性。按照规范,包名应该是小写的单个单词;且不包含
下划线或驼峰式组合。
考虑到大家在引用你的包时都需要输入包名,包名同样应简洁。
别担心包名先后引用冲突;包名只是作为包导入时默认包名;
包名不需要在所有源码中唯一,在少数导入包可能发生冲突
的情况下,可以用不同的包名作局部地代替。
无论如何,导入的包的功能决定了包名冲突较不容易发生。
另一个规范是包名应为包源码目录的基名;
包 src/pkg/encoding/base64
以如下方式被导入 "encoding/base64"
而包名应为 base64
,既非 encoding_base64
也非 encodingBase64
。
导入包后可以通过包名来引用包内容( import .
通常用于测试以及其它特殊情况,除非必要否则应尽量避免如此),
因此导出的包中的名字可以借此来避免表述不清。
例如,在 bufio
包中,缓冲读的类型为 Reader
,而不是 BufReader
,因为
调用者会使用 bufio.Reader
,这是一种更简明扼要的命名。
此外,因为被导入的实例通常基于包名调用,bufio.Reader
与 io.Reader
并不会冲突。
类似的,新建一个 ring.Ring
实例的函数—在 Go 中属于构造函数的定义—通常是 NewRing
,
但由于 Ring
是该包中的唯一的类型,它可以被命名为 New
,这样调用者就可以像这样调用 ring.New
。
灵活利用包结构可以帮助你选择更好的命名。
另一个简短的例子是 once.Do
; once.Do(setup)
表述足够清晰,
使用 once.DoOrWaitUntilDone(setup)
完全就是画蛇添足。
长命名并不会使其更具可读性。如果命名需要传达一些复杂或者是巧妙的含义,通常较好的办法
是写一份有用的说明文档而不是试图让名称承载所有信息。
获取值
Go 语言并不主动提供“设置(set)”以及“获取(get)”的支持。
你应该自己提供“设置”和“获取”,而且通常这很值得去做,但这既非惯用法也不意味着需要将 Get
放入“获取者”的名字中。
如果有一个字段为 owner
(首字母小写,未导出),“获取”方法应该为 Owner
(大写,导出),而不是 GetOwner
。默认导出大写字母开头的名字的规定提供
了区分字段和方法的便利。
如果需要提供“设置”方法, SetOwner
是一个不错的选择。两个命名看起来都很合理:
owner := obj.Owner() if owner != user { obj.SetOwner(user) }
接口命名
按规范,只包含一种方法的接口的命名应该为其方法名称加上-er后缀:
Reader
, Writer
, Formatter
等。
有许多诸如此类的命名,遵循它们以及它们所代表的函数功能会让事情变得更简单。
Read
, Write
, Close
, Flush
,
String
等等具有典型的签名及意义。为了避免冲突,只在你确定你提供的方法
具有相同的签名及意义时才为你的方法赋予这些命名。
另一方面,如果你实现的类型中的方法与某个众所周知的类型中的方法具有相同含义,那就使用相
同的命名;将字符串转换方法命名为 String
而不是 ToString
。
驼峰式命名
最后,Go 约定使用驼峰式命名 MixedCaps
或 mixedCaps
,而不是使用
下划线组合多个单词。
分号的使用
与 C 语言相似的是,Go 的正式语法中使用分号来结束语句;
与 C 语言不相同的地方在于,这些分号并不出现在源码中。
语法解析器使用一条简单的规则自动在扫描语句时加入分号,所以源码中通常不需要使用分号。
规则如下。如果新行前的最后一个标记是标识符(包含关键字如 int
,
float64
),一个基本的字面常量如数字或字符串常量,或者是以下标记之一
break continue fallthrough return ++ -- ) }
语法解析器总是在其后加上分号。可以总结为:“如果新行紧跟在某个能够结束某种陈述
语句的标记后,就插入一个分号”。
在封闭大括号之面的分号也可以省略,因此如下语句
go func(){ for { dst <- <-src } }()
不需要添加分号。
Go 的惯用法中,分号仅出现在诸如 for
循环子句中,以区分初始化/条件/增量项。
在同一行上的多条语句也需要分号间隔,在你的代码中一样。
一条警告。你不应将结构控制( if
, for
, switch
或者
select
)的开花括号放在下一行中。如果你确实这样做了,一个分号将会自动的添加在
开花括号之前,这将出现意料之外的错误。像如下这样写
if i < f() { g() }
而不是
if i < f() // wrong! { // wrong! g() }
结构控制
Go 中的结构控制与 C 语言有许多相似之处,但其不同之处才是其独到之处。
Go 中不再使用 do
或者 while
等循环控制,只有轻量
的 for
;你可以更灵活的使用 switch
;
像 for
一样, if
和 switch
接受可选的初始化语句,
此外还有一些诸如类型判断与多路转接通信 select
等新的结构控制方式。
它们的语法略微有些不同:不需要使用括号,
且执行体必须为具有花括号的语句块。
If
在 Go 语言中,一个简单的 if
语句可以像如下这样:
if x > 0 { return y }
强制使用花括号促使你将简单的 if
语句分成了多行。然而,当执行体
中包含类似 return
或 break
等控制语句时,这种编码风格的好处
一比便知。
正因为 if
和 switch
可接受初始化语句,添加局部变量
变得十分常见。
if err := file.Chmod(0664); err != nil { log.Print(err) return err }
在 Go 语言库中,你将发现如果 if
语句不可能执行到下一条语句,
也即它的执行体将以 break
, continue
,
goto
,或 return
结束,不必要的 else
将被省略。
f, err := os.Open(name) if err != nil { return err } codeUsing(f)
这是一个一系列错误判断处理的常见例子。如果控制流程成功执行,说明程序已排除错误。
既然出错时将以 return
结束,后面的代码也就不需要加入 else
语句了。
f, err := os.Open(name) if err != nil { return err } d, err := f.Stat() if err != nil { f.Close() return err } codeUsing(f, d)
重复声明
题外话:前一段的最后一个例子展示了短声明 :=
如何使用。
调用 os.Open
的声明:
f, err := os.Open(name)
这条语句声明了两个变量, f
和 err
。接下来的几行中,
调用 f.Stat
的语句:
d, err := f.Stat()
看起来似乎声明了 d
和 err
。注意,尽管 err
出现
在了两条语句中,这种重复是合法的: err
在第一次出现时被声明,但第二次时仅仅是
重新赋值。也就是说调用 f.Stat
的语句使用已被声明的变量
err
,仅仅只是赋予它一个新值。
使用 :=
声明时,一个声明过的变量 v
仍有可能出现,假设:
- 本次声明与已有的声明属于同一作用域中(如果
v
已经在更外层的作用域中声明过,
那么此次声明会创建一个新的变量), - 那么在初始化中的值将被赋给
v
, - 并且在此次声明中至少有另外一个变量是新声明的。
?【与重复声明不同,这里指第一次声明或之前的声明已失效,译者注】
这个特性简直就是纯粹的实用主义的体现,
使得我们可以很方面地只使用一个 err
值,
例如,在一个相当长的 if-else
语句链中。
你会发现这出现的很频繁。
For
Go 中的 for
循环与 C 相似,但并不完全相同。
它将 for
与 while
结合了起来,没有了 do-while
形式。
总共有三种循环格式,只有其中一种需要使用分号。
// Like a C for for init; condition; post { } // Like a C while for condition { } // Like a C for(;;) for { }
短声明使得我们能够在循环中声明索引变量。
sum := 0 for i := 0; i < 10; i++ { sum += i }
如果你需要遍历数组、切片、字符串或者映射,或者从信道中读取消息,
range
子句能够帮你轻松实现循环。
for key, value := range oldMap { newMap[key] = value }
如果你只需要使用range遍历返回的第一个项(可能是键或者索引号),那么把第二个值丢弃:
for key := range m { if expired(key) { delete(m, key) } }
如果你只需要第二个项(值),使用空白标识符,一条下划线,从而丢弃第一个值:
sum := 0 for _, value := range array { sum += value }
如果对字符串进行循环遍历,通过解析UTF-8字符将每个Unicode编码的字符解析出来, range
能够提供更多的便利。而错误的编码字符将占用一个字节并且使用U+FFFD占位符来替换它。
循环:
for pos, char := range "日本語" { fmt.Printf("character %c starts at byte position %d\n", char, pos) }
将打印:
character 日 starts at byte position 0 character 本 starts at byte position 3 character 語 starts at byte position 6
最后,Go 中没有逗号操作符,且自增 ++
、自减 --
为语句而非表达式。
因此,如果需要在 for
中使用多个变量,应该采用平行赋值的方法。
// 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 中的更为普遍。
它的表达式可以不必为常量或者甚至是整型,cases语句将从上往下逐一计算直到
匹配,如果 switch
后没有表达式,它将匹配 true
。
因此,我们可以使用 switch
替换
if
– else
– if
– else
链,
这也更符合 Go 的语言风格。
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 }
switch
没有自动的下溯,但我们能够在cases语句中
使用逗号分隔符来列举相同处理情况的条件。
func shouldEscape(c byte) bool { switch c { case ' ', '?', '&', '=', '#', '+', '%': return true } return false }
这里有一个使用两个 switch
语句来比较字节数组的例程。
// Compare returns an integer comparing the two byte arrays, // lexicographically. // The result will be 0 if a == b, -1 if a < b, and +1 if a > b func Compare(a, b []byte) int { for i := 0; i < len(a) && i < len(b); i++ { switch { case a[i] > b[i]: return 1 case a[i] < b[i]: return -1 } } switch { case len(a) < len(b): return -1 case len(a) > len(b): return 1 } return 0 }
switch语句同样可以用于判断一个接口类型变量的动态类型。类型选择使用
关键词 type
包含于闭括弧中(.(type))的类型断言语法。
如果switch语句在表达式中声明了一个变量,那么该变量在每个分支子句中都
有该变量对应的类型。
switch t := interfaceValue.(type) { default: fmt.Printf("unexpected type %T", t) // %T prints type case bool: fmt.Printf("boolean %t\n", t) case int: fmt.Printf("integer %d\n", t) case *bool: fmt.Printf("pointer to boolean %t\n", *t) case *int: fmt.Printf("pointer to integer %d\n", *t) }
函数
多值返回
Go 语言的其中一个与众不同的特性是它的函数以及方法可以有多个返回值。
这种形式可以用于避免 C 语言程序中大量臃肿的惯用法:将错误值返回(例如用 -1
表示 EOF
)
以及修改传入的实参。
在 C 语言中,写操作发生错误时将返回一个负的字节数,并且错误码被藏在了一个秘密
的不确定的位置。
然而,在 Go 语言中, Write
可以返回写入字节数以及错误值。
“是的,你写入了一些字节但并未全部写入,因为设备已经被填满了”。
在 os
包中, File.Write
的签名如下:
func (file *File) Write(b []byte) (n int, err error)
正如文档中所提,它将返回写入字节数,并在 n
!=
len(b)
时返回一个非 nil
的 error
错误值。
这是一种常见的编码风格,你可以在错误处理一节看到更多例子。
我们可以提供一种简单的方法来避免为了模拟一个引用参数而传入一个指针值。
这里有个简单的函数,从字节数组中的某个索引中获得其值,返回该值以及下一个索引。
func nextInt(b []byte, i int) (int, int) { for ; i < len(b) && !isDigit(b[i]); i++ { } x := 0 for ; i < len(b) && isDigit(b[i]); i++ { x = x*10 + int(b[i])-'0' } return x, i }
你可以像如下这样将数字扫描到数组 a
中:
for i := 0; i < len(a); { x, i = nextInt(a, i) fmt.Println(x) }
可命名的返回参数
在 Go 语言中,我们能够赋予函数的返回值或返回参数一个名称,并将其看做是一个普通的变量,
就像传入参数一样。一旦返回参数被命名,这些参数将在函数开始执行之前被初始化为其类型对应
的零值;如果函数中执行了一条不带任何参数的 return
语句,那么这些返回参数的当前值
将被返回。
这些命名并非是必须遵循的,但它们能够让代码看起来更加简洁:它们文档化的。
如果我们命名 nextInt
的返回参数,这将使得它对应的 int
返回参数值如其意。
func nextInt(b []byte, pos int) (value, nextPos int) { ... }
正因为命名返回参数已被初始化,且在函数的不带参数的返回语句中,这些命名返回参数的当前值将默认被返回。
这个特性可使我们的代码简洁而又清晰。
这里有一个很好的例子 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
语句将函数调用(被推迟执行的函数)推迟到
执行 defer
的当前函数返回之前才执行。这个非同寻常的方法,在
处理一些诸如资源在不论函数从哪个分支返回都必须先被释放的情形时,却显得非常高效。
比较典型的例子有解锁互斥锁和关闭文件。
// 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. }
推迟一个诸如 Close
的函数调用有两点好处。第一,它确保你永远不会忘记关闭这个文件,
如果你在以后又添加了一个函数返回分支时,这个错误常有出现。第二,这意味着“关闭”与“打开”比较接近,
这总比将它放在函数的最后会好。
被推迟执行的函数的实参(如果函数是一个方法,这也包含该方法的接收者)在推迟【指“推迟”这个动作,译者注】
执行时被计算,而不是函数调用时计算。除去避免担心函数在执行时参数值已发生改变之外,这还意味着
同一条推迟调用语句可以推迟多个函数的执行。这里有一个非常简单的例子。
for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) }
被推迟的函数通常以LIFO的顺序被执行,所以这段代码在函数返回时会输出 4 3 2 1 0
。
一个更具实际意义的应用是跟踪函数执行。我们可以写一对简单的跟踪例程:
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.... }
我们可以充分利用这个事实,即被推迟的函数的实参在 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() { defer un(trace("b")) fmt.Println("in b") a() } func main() { b() }
输出
entering: b in b entering: a in a leaving: a leaving: b
对于那些习惯于其它语言中块级资源管理的程序员而言, defer
看起来似乎很罕见,
但它的有趣而强大的应用源于它不是基于块的,而是基于函数的。
在 panic
和 recover
这一节中,我们将看到关于它的可能性的其他一些例子。
数据
new
分配
Go 语言中提供两种分配的操作原语,内建的两个函数 new
和 make
。
它们应用于不同的类型,实现不一样的功能。或许你会有些困惑,但其中的规则非常简单。
我们先来看看 new
。
这是一个内建的分配内存的函数,但与其它语言中同名函数不同,它并不初始化内存,
而只是简单地将内存置为零。
也就是说, new(T)
为某类型 T
分配零值的内存空间,并返回它的地址,
这是类型为 *T
的值。
用 Go 语言术语表示,它返回一个指向某类型 T
已分配的,并且初始化为对应零值的内存指针。
既然由 new
返回的内存已经零值化了,当设计你自己的数据结构时,各相应类型均已被赋为
对应零值而不需要进一步的初始化,这一点非常有帮助。这意味着该数据结构的使用者可以直接利用 new
创建一个对象并且直接访问它。
例如, bytes.Buffer
文档中提到“ Buffer
的零值是一个可使用的空缓冲区。”
类似的, sync.Mutex
的零值被解释为一个未上锁的互斥锁。
“零值特性”可以带来无穷的好处。考虑如下的类型定义。
type SyncedBuffer struct { lock sync.Mutex buffer bytes.Buffer }
SyncedBuffer
类型的值在分配完内存或者声明之后即可直接被访问。
后续代码中, p
和 v
不需要进一步的处理即可正确的被访问。
p := new(SyncedBuffer) // type *SyncedBuffer var v SyncedBuffer // type SyncedBuffer
构造函数和复合字面
只是有时候零值还是不够好,因此有必要提供初始化的构造函数,正如下面这个
从 os
包中提取的例子。
func NewFile(fd int, 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}
复合字面的字段必须按顺序赋值并初始化,并且必须提供所有字段的初始值。
然而,通过字段 :
value的组合语句明确标记这些元素后,初始化时字段可以
按照任意顺序排列,并且没有明确指明的字段将被赋值为对应类型的零值。因此我们可以这样:
return &File{fd: fd, name: name}
少数情况下,如果复合字面没有提供任何字段的初始化,它将创建该类型的零值。
因此,表达式 new(File)
和 &File{}
是等价的。
复合字面同样可用于创建数组、切片以及映射,字段标记相应可以用于索引或者是映射的键。
在下列的例子中,初始化过程将会忽略 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)
的作用完全不一样。
它仅能被用于创建切片、映射以及信道,并且返回一个对应类型为 T
(而不是 *T
)
的已被初始化(非零值)的值。
不同的原因在于,这三种类型的底层实现,是对某些数据结构的引用,必须在使用前被初始化。
例如,一个切片,是一个具有三个项的描述符:指向数据的指针(内嵌在某个数组中),切片长度以及切片容量。
在这三个项被初始化之前,切片将始终是 nil
。
对于切片、映射以及信道, make
初始化内部的数据结构并且为实例初始化其值。
例如:
make([]int, 10, 100)
将分配一个可以包含100个整型元素的数组空间,并且在该空间中创建一个长度为10、容量为100的指向该
数组的前10个元素的切片。
(当创建一个切片时,容量可以不必指定;可以参考“切片”一节的详细说明。)
相反地, new([]int)
返回指向已分配的、零值化的切片的指针,那是一个指向值为 nil
的指针。
如下的例子很好的解释了 new
和 make
之间的区别。
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints // Unnecessarily complex: var p *[]int = new([]int) *p = make([]int, 100, 100) // Idiomatic: v := make([]int, 100)
记住, make
只能应用于映射、切片和信道,并且不返回指针。
若要明确地获得指针,应使用 new
。
数组
当你需要明确规划内存布局时,数组就显得大为有效,某种程度上还能避免过多的内存分配,
但首先它们是切片的块组建结构,这是下一节的主题。
让我们先插入一些题外话,来为它作铺垫。
Go 语言中,数组的运作原理与 C 语言有很大的不同。
在 Go 语言中,
- 数组是值。将某个数组赋值给另外一个将会复制其所有的元素。
- 特别的,如果你将一个数组作为传入参数给某个函数,函数会得到该数组的一份副本,
而不是指向该数组的指针。 - 数组的大小是数组类型的一个组成部分。类型
[10]int
与[20]int
并非是
同一种类型。
值属性是非常有用的,但有时你需要付出昂贵的代价;如果你希望它们有 C 语言那样的行为和效率,
你可以传递一个指向该数组的指针。
func Sum(a *[3]float64) (sum float64) { for _, v := range *a { sum += v } return } array := [...]float64{7.0, 8.5, 9.1} x := Sum(&array) // Note the explicit address-of operator
但这中风格同样不属于 Go 中的惯用法。切片才是。
Slices
切片对数组进行封装以提供更通用、强大以及方便的对序列数据的访问接口。
除了诸如变换矩阵那些需要明确定义维数的数据,在 Go 中的大部分数组编程是可以
使用切片来实现的。
切片是引用类型的,这意味着如果你将某个切片实例赋值给另一个切片实例,它们都将指向
同一个底层的数组结构。例如,如果一个函数将一个切片作为传入参数,在函数中改变该切片中的元素
对于调用者而言同样可见,可以理解为传递了底层的数组结构的指针。因此, Read
函数可以
接受一个切片类型的参数而不是一个指针和一个计数值;切片中的长度限制了最大能读入的数据数。
这是 os
包中提取的 File
类型的 Read
方法的签名:
func (file *File) Read(buf []byte) (n int, err error)
该方法返回读取的字节数以及错误值,如果出错的话。为了从一个更大的缓冲区 buf
中读入前32个
字节,将该缓冲区切片(这里用作动词)。
n, err := f.Read(buf[0:32])
这类切片是非常常见且高效的。事实上,如果抛开效率不说,如下的例子同样会从缓冲区中读入前
32个字节。
var n int var err error for i := 0; i < 32; i++ { nbytes, e := f.Read(buf[i:i+1]) // Read one byte. if nbytes == 0 || e != nil { err = e break } n += nbytes }
切片的长度在未超出底层数组结构的限制时是可变的;只需要将它的一份切片赋值给它自己就行了。
切片的容量,通过内建的 cap
函数可得,代表该切片的最大长度。
这里有一个将数据追加到切片的函数实现。如果数据超过了容量大小,会重新分配该切片。
并将返回最终的切片。这个函数利用了一个事实,对值为 nil
的切片调用
len
和 cap
是合法的,并返回0。
func Append(slice, data[]byte) []byte { l := len(slice) if l + len(data) > cap(slice) { // reallocate // Allocate double what's needed, for future growth. newSlice := make([]byte, (l+len(data))*2) // The copy function is predeclared and works for any slice type. copy(newSlice, slice) slice = newSlice } slice = slice[0:l+len(data)] for i, c := range data { slice[l+i] = c } return slice }
我们最终必须返回该切片因为尽管 Append
会修改 slice
的元素,该切片自身
(具有指针、长度和容量的运行时数据结构)是值传递的。
向切片添加数据的想法实在是太妙了,所以我们将它实现为内建的 append
函数。
为了理解该函数的设计,我们还需要一些额外的信息,我们稍后还会谈论这个话题。
映射
若需要将不同类型的值组织起来,映射绝对是一个方便且强大的内建数据结构。
键可以为任何等式操作符所支持的类型,例如整型、浮点、复数、字符串、指针、接口(只要
动态类型支持等式判断)、结构体以及数组。切片不能被用作映射的键,因为等式并不可用。
与切片一样,映射也是一个引用类型。如果你将映射作为函数的传入参数,函数中对映射内容的修改
对调用者同样可见。
映射能够按照一般的复合字面语法,以冒号分割的键值对进行构造,因此在初始化时
组建映射就变得简单多了。
var timeZone = map[string] int { "UTC": 0*60*60, "EST": -5*60*60, "CST": -6*60*60, "MST": -7*60*60, "PST": -8*60*60, }
对映射的值进行赋值或者取值操作看起来就像对于数组操作一样,除了一点:索
引值不要求一定是整型。
offset := timeZone["EST"]
若试图对不存在于映射中的键取值,会返回与该映射中项的类型对应的零值。
例如,某个映射包含整型,当查找一个不存在于映射中的键进行查找时会返回 0
。
集合类型可以被实现为一个值类型为 bool
的映射。
将映射中的项设置为 true
可将该项的值放入该集合中,此后通过简单的索引
操作可以判断项存在与否。
attended := map[string] bool { "Ann": true, "Joe": true, ... } if attended[person] { // will be false if person is not in the map fmt.Println(person, "was at the meeting") }
有时你需要区分某项是否不存在或者其值为零值。是否存在 "UTC"
的项且其值为零或者该项
根本不存在该映射中?你可以通过多值赋值的形式来判断。
var seconds int var ok bool seconds, ok = timeZone[tz]
显然,我们可以称之为“逗号 ok” 惯用法。
在这个例子中,如果 tz
存在, seconds
会被赋予合适
的值,且 ok
会被置为true;否则, seconds
会被赋为零,且
ok
被置为false。
这有一个函数将它与错误处理优雅的结合了起来:
func offset(tz string) int { if seconds, ok := timeZone[tz]; ok { return seconds } log.Println("unknown time zone:", tz) return 0 }
如果仅需要判断键是否存在于映射中而不需该键的对应值,你可以使用空白描述符( _
)。
空白描述符可以被赋值给任意类型或被声明为任意类型的值,它的值将被丢弃。为了判断键是否
存在于映射中,使用空白描述符替换值的位置。
_, present := timeZone[tz]
若要删除映射中的某项,使用内建的 delete
函数,它以映射以及要删除的键作为
参数。即使键不存在与映射中,该操作也是安全的。
delete(timeZone, "PDT") // Now on Standard Time
打印
在 Go 语言中,格式化打印采用类似于 C 中 printf
族的风格,但更丰富且通用。
这系列函数在 fmt
包中,函数名首字母均大写: fmt.Printf
,
fmt.Fprintf
, fmt.Sprintf
等等。
字符串函数(例如 Sprintf
)将返回一串字符串而非填充给定的缓冲区。
你无需提供一串格式化字符串。对于每个 Printf
, Fprintf
和
Sprintf
,都有一个对应的函数,例如 Print
和 Println
。
这些函数并不接受格式化字符串,相反它们会为每个参数生成默认的格式。 Println
系列的
函数还会在两个参数间插入空格,并在输出时追加一个换行符,然而 Print
系列的函数仅在
操作对象的两侧均不为字符串时才加入空格。
以下示例所有行都会产生相同的输出。
fmt.Printf("Hello %d\n", 23) fmt.Fprint(os.Stdout, "Hello ", 23, "\n") fmt.Println("Hello", 23) fmt.Println(fmt.Sprint("Hello ", 23))
正如在Go 语言之旅 中提到的,类 fmt.Fprint
的函数
的第一个参数必须实现 io.Writer
接口;变量 os.Stdout
和 os.Stderr
都是为人熟知的实例。
从这开始,事情将变得与 C 语言不一样。首先,如 %d
的数值格式并不接受符号或者大小的标志;相反
打印例程根据参数的类型来决定这些属性。
var x uint64 = 1<<64 - 1 fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
输出
18446744073709551615 ffffffffffffffff; -1 -1
如果你希望使用默认的转换,例如整型的十进制表示,你可以“垃圾桶”格式 %v
(表示 “value”);结果与 Print
和 Println
的输出完全相同。
此外,该格式还能打印任意类型值,甚至包括数组、结构体和映射。
这有一个打印前一节中时区映射的语句。
fmt.Printf("%v\n", timeZone) // or just fmt.Println(timeZone)
将输出
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
当然,对于映射而言,键的输出顺序可能是随机的。
当打印结构体时,改进的格式 %+v
为结构体的每个字段添上字段名,而另一个格式 %#v
则将完全按照 Go 的语法打印值。
type T struct { a int b float64 c string } t := &T{ 7, -2.35, "abc\tdef" } fmt.Printf("%v\n", t) fmt.Printf("%+v\n", t) fmt.Printf("%#v\n", t) fmt.Printf("%#v\n", timeZone)
将打印
&{7 -2.35 abc def} &{a:7 b:-2.35 c:abc def} &main.T{a:7, b:-2.35, c:"abc\tdef"} map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}
(注意取值符号&)
使用 %q
时, string
或者 []byte
类型的值能够
产生带引号的字符串;而可选的 %#q
将尽可能使用反引号替代引号。
此外, %x
对于字符串和字节数组有着与整型类似的效果,都将产生长的十六进制字符串,
并且对于带空格的格式( % x
)中,它还会在字节间插入空格。
另一个实用的格式是 %T
,它将打印某个值的类型。
fmt.Printf("%T\n", timeZone)
输出
map[string] int
如果你想控制自定义类型的默认格式,只需为该类型定义一个具有签名为 String() string
的方法。
对于一个简单的类型 T
,它应该是这样的:
func (t *T) String() string { return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c) } fmt.Printf("%v\n", t)
会以如下格式打印
7/-2.35/"abc\tdef"
(如果你需要像打印 T
的指针类型值那样打印 T
的类型值,那么 String
方法的接收者必须为值类型;该例使用指针因为指针操作对于结构体类型而言更为高效且通用。
更多详情参见指针 vs. 值 的接收者一节。)
我们的 String
方法同样能够调用 Sprintf
因为打印例程是完全可重入的,且能够
被递归调用。
我们甚至还能做些更高级的操作,直接将打印例程的实参传递给另一类似的例程。
Printf
的签名接受类型为 ...interface{}
作为它的最后一个形参,来表明能够在格式化字符串后
传入可变参数列表。
func Printf(format string, v ...interface{}) (n int, err error) {
在 Printf
函数中, v
看起来更像是 []interface{}
的类型的变量,
但如果将它传递给另一个可变参数函数,它则像是常规的实参列表。
以下是我们之前用过的 log.Println
的实现。它直接将它的参数传递给 fmt.Sprintln
作为实际的格式化参数。
// Println prints to the standard logger in the manner of fmt.Println. func Println(v ...interface{}) { std.Output(2, fmt.Sprintln(v...)) // Output takes parameters (int, string) }
在这个嵌套调用中,我们将 ...
写在 v
以调用 Sprintln
是为了
告诉编译器将 v
视为一个参数列表,否则它会将 v
当做单一的切片参数。
还有很多关于打印的知识点我们没有提到。详情请参阅 godoc
对 fmt
包的具体说明文档。
顺便提一句, ...
参数可以为一个特定的类型,例如对于一个以一系列整数为形参的最小值函数,
形参可以为 ...int
。
func Min(a ...int) int { min := int(^uint(0) >> 1) // largest int for _, i := range a { if i < min { min = i } } return min }
追加
现在我们可以将遗漏的关于内建的 append
函数的设计给补上了。
append
的签名不同于我们之前自定义的 Append
函数。
大致地说,它看起来像这样:
func append(slice []T, elements...T) []T
其中T为任意给定类型的占位符。事实上,在 Go 语言中,你不能写一个参数类型 T
由调用者决定的函数。
这就是为何 append
是内建函数的原因:它需要编译器的支持。
append
将元素追加切片的末尾并返回结果。
我们需要返回结果,因为,与我们自定义的 Append
一样,底层的
数组可能会被改变。这个简单的例子
x := []int{1,2,3} x = append(x, 4, 5, 6) fmt.Println(x)
将打印 [1 2 3 4 5 6]
。因此 append
的效果类似于 Printf
,都可接受任意数量的实参。
但如果我们希望像 Append
那样将一个切片追加到另一个切片中呢?
非常简单:像我们在调用 Output
那样,在调用时使用 ...
。
以下代码段的输出与前一个一致。
x := []int{1,2,3} y := []int{4,5,6} x = append(x, y...) fmt.Println(x)
如果没有 ...
,这段代码会由于类型错误而无法编译; y
的
类型不是 int
。
初始化
尽管从表面上看,Go 语言中的初始化过程看起来与 C 或 C++ 并没有什么太大的不同,
但它确实更为强大。
复合结构体能够在初始化的时候被创建,并且不同包中的对象的初始化顺序能够被
正确处理。
常量
Go 中的常量就是——不变的量。
它们将在编译时被创建,即便它们可能是函数内定义的局部变量。
常量仅能为数字、字符串或者布尔值。
由于编译时创建的限制,定义常量的表达式也必须为常量表达式,也就是说它们对于
编译器必须是可求值的。例如, 3<<3
属于常量表达式,而
math.Sin(math.Pi/4)
就不是,因为 math.Sin
函数调用仅仅在
运行时才发生。
在 Go 中,枚举常量可以使用 iota
枚举器。
因为 iota
可以是表达式的一部分而表达式能够被隐式地重复,建立
复杂的值的集合就非常轻松了。
type ByteSize float64 const ( _ = iota // ignore first value by assigning to blank identifier KB ByteSize = 1 << (10 * iota) MB GB TB PB EB ZB YB )
由于能够将某种方法比如 String
附在一些类型上,使得在打印时自动地格式化这些类型的值
变得可能了,自动格式化打印甚至可以成为一个通用类型的一部分。
func (b ByteSize) String() string { switch { case b >= YB: return fmt.Sprintf("%.2fYB", b/YB) case b >= ZB: return fmt.Sprintf("%.2fZB", b/ZB) case b >= EB: return fmt.Sprintf("%.2fEB", b/EB) case b >= PB: return fmt.Sprintf("%.2fPB", b/PB) case b >= TB: return fmt.Sprintf("%.2fTB", b/TB) case b >= GB: return fmt.Sprintf("%.2fGB", b/GB) case b >= MB: return fmt.Sprintf("%.2fMB", b/MB) case b >= KB: return fmt.Sprintf("%.2fKB", b/KB) } return fmt.Sprintf("%.2fB", b) }
表达式 YB
会打印出 1.00YB
,而 ByteSize(1e13)
会
打印出 9.09
。
注意,在 String
方法的实现中调用 Sprintf
类的函数
是没有问题的,但要警惕使用格式化字符串( %s
, %q
, %v
,
%x
or %X
)嵌套调用 Sprintf
将导致
String
方法的递归调用。
ByteSize
的 String
方法实现是安全的,因为它使用 %f
调用
Sprintf
函数。
变量
变量能够像常量一样被初始化,但其初始值还可以是在运行时才被计算的一般表达式。
var ( home = os.Getenv("HOME") user = os.Getenv("USER") goRoot = os.Getenv("GOROOT") )
init 函数
最后,每个源文件都可以通过定义自己的零值 init
函数来设置一些必要的状态。
(事实上,每个文件都可包含多个 init
函数。)
最后的最后:仅在包中的所有定义的变量都使用初始值初始化后 init
才会被调用,
而那些初始值仅在所有导入包都被初始化后才被计算。
包括那些不能被表示为声明的初始化, init
还通常被用于在程序真正执行前
检验或者校正程序的状态。
func init() { if user == "" { log.Fatal("$USER not set") } if home == "" { home = "/home/" + user } if goRoot == "" { goRoot = home + "/go" } // goRoot may be overridden by --goroot flag on command line. flag.StringVar(&goRoot, "goroot", goRoot, "Go root directory") }
方法
指针 vs. 值
任何非指针或接口类型的命名类型都能够定义自己的方法,方法的接收者可以不必为结构体。
在之前讨论切片时,我们写了一个 Append
函数。我们还可以将它定义为切片的
方法。为此,我们首先定义一个能够绑定该方法的命名类型,并且使该方法的接收者为该类型的值。
type ByteSlice []byte func (slice ByteSlice) Append(data []byte) []byte { // Body exactly the same as above }
我们仍然需要这个方法返回更新后的切片。我们可以通过重新定义该方法,将该方法的
接收者为一个指向 ByteSlice
的指针来而避免采用之前笨拙的手法,
这样该方法就能重写调用者提供的切片了。
func (p *ByteSlice) Append(data []byte) { slice := *p // Body as above, without the return. *p = slice }
事实上,我们能够做得更好。如果我们将函数修改为与标准的 Write
类似的方法,
就像这样,
func (p *ByteSlice) Write(data []byte) (n int, err error) { slice := *p // Again as above. *p = slice return len(data), nil }
那么 *ByteSlice
类型就满足了标准的 io.Writer
接口,这将非常实用。
例如,我们可以通过打印将内容写入。
var b ByteSlice fmt.Fprintf(&b, "This hour has %d days\n", 7)
我们将 ByteSlice
的地址作为实参,因为只有 *ByteSlice
才满足
io.Writer
。以指针或值为接收者的区别在于,值方法能够以指针类型和值类型的方式调用,
而指针方法仅能以指针类型被调用。因为指针方法能够修改接收者;而在值的副本上所做的修改将被丢弃。
顺便提一句, bytes.Buffer
的实现同样采用在字节切片上实现 Writer
的思路。
接口和其它类型
接口
Go 中的接口类型为描述对象行为提供了可能:如果某个对象可以做这件事,那么
它能够被用在这里。我们已经看了许多简单的例子;通过实现 String
方法,我们可以自定义格式化打印,而通过 Write
方法, Fprintf
能够
为任何对象提供输出。
在 Go 代码中,仅包含一两种方法的接口是很常见的,并且接口通常按照其实现的方法来命名,例如
io.Writer
表示实现了 Write
的一类对象。
每种类型都能实现多种接口。
例如,如果实现了包含 Len()
,
Less(i, j int) bool
, and Swap(i, j int)
的
sort.Interface
,一个收集器就可以使用 sort
包中提供的
例程进行排序,它同样可以有一个自定义格式化形式。
在有意编写的 Sequence
例子中,它就满足这两点。
type Sequence []int // Methods required by sort.Interface. func (s Sequence) Len() int { return len(s) } func (s Sequence) Less(i, j int) bool { return s[i] < s[j] } func (s Sequence) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // Method for printing - sorts the elements before printing. func (s Sequence) String() string { sort.Sort(s) str := "[" for i, elem := range s { if i > 0 { str += " " } str += fmt.Sprint(elem) } return str + "]" }
转换
Sequence
的 String
方法重新实现了 Sprint
已为切片实现的功能。
如果在调用 Sprint
之前先将 Sequence
转换为
纯粹的 []int
,我们就可以利用已经实现的功能。
func (s Sequence) String() string { sort.Sort(s) return fmt.Sprint([]int(s)) }
通过将 s
强制转换为普通的切片, s
就能够接收默认的格式化。
如果未经转换, Sprint
会发现 Sequence
的 String
方法,
导致不确定的递归调用。
因为如果我们不考虑类型名,这两种类型( Sequence
和 []int
)是相同的,
所以转换是合法的。
转换过程并不会创建一个新的值,它只是使现有类型暂时看起来有个新类型。
(有一些其他的合法转换过程,例如从整型转换到浮点型,则会创建一个新的值)
为了访问一组不同类型的方法而转换表达式的类型在 Go 中非常常见。
例如,我们可以利用现有的 sort.IntSlice
类型来减少代码量:
type Sequence []int // Method for printing - sorts the elements before printing func (s Sequence) String() string { sort.IntSlice(s).Sort() return fmt.Sprint([]int(s)) }
现在,利用数据项能够转换成其他多种类型( Sequence
, sort.IntSlice
and []int
)的特性,每种类型实现一部分方法,我们完全避免了实现多种接口。
这在实战中虽然不同寻常,但却很高效。
概论
如果某种类型存在但只实现一种接口,且除了该接口没有导出任何方法,
那么就没有必要导出该类型。
仅仅导出该接口能让我们更专注于它的行为,而不是它的实现,并且其它具有不同属性
的实现可以从最原始的类型中借鉴。
这还能够避免为每个通用的方法的实力编写重复文档。
在这种情况下,构造函数应该返回一个接口值而非实现的类型。
例如,在hash库中, crc32.NewIEEE
和 adler32.New
都返回接口类
型 has.Hash32
。在 Go 程序中将算法从Adler-32替换为CRC-32只需要修改
构造函数的调用即可;而其余的代码则不受算法改变的影响。
同样的方式使得 crypto
包中能够将流加密算法与块加密算法分开。
crypto/cipher
包中的 Block
接口指明了块加密算法的行为,它为
单独的块数据提供加密。
然后,与 bufio
包相似,任何实现了这个接口的加密包都能被用于构成以
Stream
为接口的流式加密,而不需要了解关于块加密的细节。
The crypto/cipher
interfaces look like this:
type Block interface { BlockSize() int Encrypt(src, dst []byte) Decrypt(src, dst []byte) } type Stream interface { XORKeyStream(dst, src []byte) }
这是计数器模式CTR流的定义,它将块加密改为流加密;注意块加密的细节已被抽象化了。
// NewCTR returns a Stream that encrypts/decrypts using the given Block in // counter mode. The length of iv must be the same as the Block's block size. func NewCTR(block Block, iv []byte) Stream
NewCTR
的应用并不局限于特定的加密算法和数据源,它适合于任何实现了 Block
接口和 Stream
类型的情形。
因为它们返回了接口值,将CTR加密替换为其它的加密模式只需做局部的更改。构造函数的调用过程
必须被修改,但由于其周围的代码将结果看做是一个I Stream
,它们不会注意到有什么改动。
接口和方法
因为几乎任何类型都能添加方法,所以几乎任何类型都满足接口的要求。
一个很直观的例子是在 http
包中定义的 Handler
接口。
任何实现了 Handler
接口的对象均能够处理HTTP请求。
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
ResponseWriter
本身是一个接口,它提供了对方法的访问,这些方法需要返回响应给客户端。
这些方法包含了标准的 Write
方法,因此一个 http.ResponseWriter
可
被用于任何 io.Writer
适用的场景。
Request
是一个包含从客户端发出的已解析的请求结构。
简单起见,我们假设所有HTTP请求都是GET方法,而忽略POST方法。
这样简化并不影响处理句柄的建立方式。
这里有一个短小却完整的关于计算某个页面被访问次数的处理句柄的实现。
// Simple counter server. type Counter struct { n int } func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctr.n++ fmt.Fprintf(w, "counter = %d\n", ctr.n) }
(紧跟上我们的主题,注意 Fprintf
如何能输出到 http.ResponseWriter
。)
作为参考,这里演示了如何将这样一个服务器添加到URL树的一个节点上。
import "net/http" ... ctr := new(Counter) http.Handle("/counter", ctr)
但为什么 Counter
要是一个结构体呢?一个整型就足够了。
(接收者必须为指针,增量操作对于调用者才可见。)
// Simpler counter server. type Counter int func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { *ctr++ fmt.Fprintf(w, "counter = %d\n", *ctr) }
当页面被访问时怎样通知你的程序去更新一些内部状态呢?为web页面绑定一个信道吧。
// A channel that sends a notification on each visit. // (Probably want the channel to be buffered.) type Chan chan *http.Request func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) { ch <- req fmt.Fprint(w, "notification sent") }
最后,假设我们需要输出调用服务器二进制程序时使用的实参 /args
。这很简单。
func ArgServer() { for _, s := range os.Args { fmt.Println(s) } }
我们怎么将它转换为一个HTTP服务器呢?我们能够将 ArgServer
实现为某种
值可忽略的类型的方法,但有一个更简单的方法。
既然我们可以为任何除了指针和接口之外的任何类型定义一个方法,我们同样可以为一个函数添加一个
方法。
http
包中包含了以下代码:
// HandlerFunc 类型是一个允许普通函数作为HTTP处理句柄的适配器。 // 如果f是一个具有恰当签名的函数,HandlerFunc(f)就是一个调用f的Handler对象。 type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(c, req). func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) { f(w, req) }
HandlerFunc
是一个包含 ServeHTTP
方法的类型,所以该类型
的值就可以处理HTTP请求。
我们来看看方法的实现:接收者是一个函数 f
,而该方法调用该函数 f
。
这很奇怪但不值得大惊小怪的,区别在于接收者成为了一个信道,而方法通过信道发送消息。
为了将 ArgServer
实现为一个HTTP服务器,我们首先得让它拥有合适的签名。
// Argument server. func ArgServer(w http.ResponseWriter, req *http.Request) { for _, s := range os.Args { fmt.Fprintln(w, s) } }
现在,<codeArgServer 具有了与 HandlerFunc
同样的签名,
所以我们可以将它转换为这种类型以访问它的方法,就像我们将 Sequence
转换
为 IntSlice
以访问 IntSlice.Sort
一样。
建立代码是很简单的:
http.Handle("/args", http.HandlerFunc(ArgServer))
当有人访问 /args
页面时,安装的处理句柄有值 ArgServer
和
类型 HandlerFunc
。
HTTP服务器会在 HandlerFunc。ServeHTTP
中,
以 ArgServer
为接收者调用 ServeHTTP
方法,
反过来将调用 ArgServer
(通过 f(c, req)
)。
进而参数就将被打印出来。
在这一节中,我们利用一个结构体、一个整型、一个信道和一个函数,建立了一个HTTP服务器,
这只因为接口仅代表方法的集合,几乎任何类型都可以定义接口。
内嵌
Go 语言不提供典型的、类型驱动型的子类化概念,但通过将类型内嵌到一个结构体或者
接口的方法中它就能够“借鉴”部分实现。
接口内嵌是很简单的。
我们之前已提到过 io.Reader
和 io.Writer
接口;
这里是它们的定义。
type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) }
io
包同样导出了一些其它的接口,以阐明对象所需要实现的一些方法。
例如, io.ReadWriter
就是一个包含 Read
和 Write
的接口。我们能够通过显式地列出这两个方法来明确指明 io.ReadWriter
,但
通过内嵌这两个接口组成一个新接口显然来得快些而且更具启发性,就像这样:
// ReadWriter is the interface that combines the Reader and Writer interfaces. type ReadWriter interface { Reader Writer }
正如它看起来那样: ReadWriter
能够做任何 Reader
和 Writer
能够做的事;它是内嵌接口的联合体(必须是不相交的方法的集合)
只能接口能够被嵌入接口中。
同样的想法可以应用在结构体中,但其意义更深远。 bufio
包有两个结构体类型,
bufio.Reader
和 bufio.Writer
,每一个接口都实现了与 io
包中
相同意义的接口。
此外, bufio
还通过使用结合 reader/writer
并内嵌到结构体中实现了带缓冲的 reader/writer
:
它列出了结构体中的两种类型,但并没有提供字段名称。
// ReadWriter stores pointers to a Reader and a Writer. // It implements io.ReadWriter. type ReadWriter struct { *Reader // *bufio.Reader *Writer // *bufio.Writer }
内嵌的元素为指针,且在它们被调用前必须先被初始化为指向某个有效的结构的指针。
<codeReadWriter 结构体能以如下方式定义
type ReadWriter struct { reader *Reader writer *Writer }
但为了提升该字段的方法以及满足 io
接口,我们同样需要
一个转发的方法,就像这样:
func (rw *ReadWriter) Read(p []byte) (n int, err error) { return rw.reader.Read(p) }
而通过直接内嵌结构体,我们就可以避免如此繁琐。
内嵌类型的方法可以直接引用,这意味着 bufio.ReadWriter
不仅包括 bufio.Reader
和 bufio.Writer
方法,它还满足下列三个接口:
io.Reader
,
io.Writer
,和
io.ReadWriter
。
有一个区分内嵌与子类的重要手段。当内嵌一个类型时,该类型的方法成为了外部类型的方法,
但当它们被调用时,该方法的的接收者是内部的类型,而不是外部的。
在我们的例子中,当 ReadWriter
的 Read
方法被调用时,它与之前写的转发
方法具有完全一样的效果;接收者是 ReadWriter
的 reader
字段,而不是 ReadWriter
本身。
内嵌同样可以提供便利。
这个例子展示了一个内嵌字段和一个常规的命名字段。
type Job struct { Command string *log.Logger }
<codeJob 类型现在有了 Log
, Logf*log.Logger
的其它方法。
我们当然能够为 Logger
提供一个字段名,但完全没有必要这么做。
现在,一旦初始化后,我们可以记录 Job
:
job.Log("starting now...")
Logger
是一个结构体的常规字段,我们能够使用普通的构造器来初始化它,
func NewJob(command string, logger *log.Logger) *Job { return &Job{command, logger} }
或者是通过复合字面,
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
如果我们需要直接引用一个内嵌的字段,可以忽略包限定词,直接将字段类型名作为字段名。
如果我们需要访问 Job
的某个变量 job
的 *log.Logger
,可以
直接写作 job.Logger
。
如果我们想精炼 Logger
的方法时,这会十分有用的。
func (job *Job) Logf(format string, args ...interface{}) { job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...)) }
内嵌类型会引入了命名冲突的问题,但解决规则是很容易的。
首先,一个字段或方法 X
会隐藏了该类中更深层嵌套的其它项 X
。
如果 log.Logger
包含了名为 Command
的一个字段或方法, Job
的
Command
字段覆盖它。
其次,如果同名冲突出现在嵌套的同一级,通常会产生一个错误;
如果将 log.Logger
内嵌到包含了名为 Logger
的字段或方法的 Job
结构,会产生错误。
然而,如果重复的名字永远不会出现在类型定义的程序块之外,那就不会出错。
这个限定条件为从外部嵌套的类型发生修改时提供了某种保护;因此,如果两个相同的字段永远不会被调用,就可以将一个
字段添加到同一子类中具有冲突字段的结构中。
并发
通过通信来共享
并发编程是一个大论题,但篇幅有限,这里仅讨论一些 Go 特有的东西。
正因为要精确的控制对共享变量的访问顺序,并发编程在许多环境中非常困难。
Go 语言提倡另辟蹊径,共享变量值将通过信道进行传递,事实上,永远不会被多个单独执行
的线程所共享访问。在任意给定的时间点,只有一个给定的goroutine能够访问这个值。
数据竞争从设计上就被杜绝了。
为了提倡这种思考方式,我们将它简化为一句偈语:
不通过共享内存来通信,而通过通信来共享内存。
这个方法的意义深远。例如,引用计数值可以通过为整型变量添加一个互斥锁来很好的实现。
但作为一种高级方法,使用信道来控制访问能够让你写出更简洁、正确的程序。
我们可以从典型的单线程运行在单CPU之上的情形来审视这种模型。
它不需要提供同步原语。现在考虑另一种情况;它完全不需要同步。现在让它们俩进行通信;
如果将通信过程看做同步者【时钟相对论,译者注】,那就完全不需要其他多余的同步了。
例如,Unix中的管道,就与这种模型完美契合。尽管 Go 的并发处理方法来源于Hoare的通信时序处理(CSP),
它依然可以看做是Unix中管道的类型安全的实现。
Go routines
我们称之为goroutines因为所有现有的术语—线程、协程、进程等—
不能够传达准确的含义。goroutine具有简单的模型:它是一个与其它goroutine并发运行在
同一个地址空间的函数。它是轻量级的,所有的消耗几乎就只有栈空间的分配。
而且栈最开始是非常小的,所以它们很廉价,仅在需要的时候才会在堆空间分配(和释放)而变化。
Goroutine 在多线程操作系统上可实现多路复用,因此如果一个线程阻塞,比如说等待I/O,那么其它的
线程就会运行。Goroutine 的设计隐藏了线程创建和管理的诸多复杂性。
在函数或者方法调用前添加 go
关键字前缀能够让该次调用运行过在一个新的
goroutine中。当调用结束时,这个goroutine会静静地退出。(效果有点像Unix shell中
的 &
符号,它能够让命令在后台运行。)
go list.Sort() // run list.Sort concurrently; don't wait for it.
在goroutine调用中函数字面非常实用。
func Announce(message string, delay time.Duration) { go func() { time.Sleep(delay) fmt.Println(message) }() // Note the parentheses - must call the function. }
在 Go 语言中,函数字面都是闭包:其实现保证了函数内引用的变量的生命周期与函数的活动周期相同。
这些函数没有太大的实用性,因为这些函数没有实现完成时的信号处理。因此,我们需要信道。
信道
与映射一样,信道也属于引用类型,通过 make
进行分配。
如果提供了一个可选的整型参数,则将使用该参数作为该信道的缓冲区大小。
默认的值为零,表示一个不带缓冲区的或者是同步信道。
ci := make(chan int) // unbuffered channel of integers cj := make(chan int, 0) // unbuffered channel of integers cs := make(chan *os.File, 100) // buffered channel of pointers to Files
信道通过结合—值的交换—和同步—之间的通信,确保了两次计算过程(在不同goroutine间)
均处于可靠的状态。
关于信道的使用有许多惯用法。我们可以从下面这个开始了解。
在前一节中我们在后台启动了排序操作。信道使得启动该排序的goroutine的程序能够等待排序的结束。
c := make(chan int) // Allocate a channel. // Start the sort in a goroutine; when it completes, signal on the channel. go func() { list.Sort() c <- 1 // Send a signal; value does not matter. }() doSomethingForAWhile() <-c // Wait for sort to finish; discard sent value.
接收者总是阻塞直至有消息可接收。
如果信道是不带缓冲的,那么发送消息者也会阻塞直到接收者接收了消息。
如果信道是带缓冲的,发送者仅在消息被拷贝到缓冲区之前阻塞;如果缓冲区已满,意味
着发送者将等待直到某个接收者接收了消息。
带缓冲区的信道可以被用作信号量,例如可以限制吞吐。
在这个例子中,到来的请求被传递给 handle
,它将消息值发送到信道,处理请求
并从信道中接收返回值。
信道缓冲区的大小限制了对 process
的同步调用次数。
var sem = make(chan int, MaxOutstanding) func handle(r *Request) { sem <- 1 // Wait for active queue to drain. process(r) // May take a long time. <-sem // Done; enable next request to run. } func Serve(queue chan *Request) { for { req := <-queue go handle(req) // Don't wait for handle to finish. } }
这里有同样的但通过建立固定数量的从信道中接收请求并处理的goroutine的实现.
goroutine的数量限制了同步调用 process
的数量。
Serve
函数同样接收一个可用于被告知退出的信道,在启动所有goroutine之后,
它将阻塞并暂停从信道中接收消息。
func handle(queue chan *Request) { for r := range queue { process(r) } } func Serve(clientRequests chan *Request, quit chan bool) { // Start handlers for i := 0; i < MaxOutstanding; i++ { go handle(clientRequests) } <-quit // Wait to be told to exit. }
信道中的信道
信道属于一级类的值,能够被分配并像其它一级类的值一样被传递,这是
Go 中一个非常重要的特性。通常可以利用这个特性实现安全、并行的多路分解。
在前一节的例子中, handle
是一个非常理想化的请求处理句柄,但我们
没有定义它所处理的请求的类型。如果请求类型包含一个可于回复的信道,那么每个客户端
都能够为服务器提供自己的回复路径。
这里有一个简要的 Request
的定义。
type Request struct { args []int f func([]int) int resultChan chan int }
客户端在请求对象中提供了一个处理函数和参数,以及一个接收回复的信道。
func sum(a []int) (s int) { for _, v := range a { s += v } return } request := &Request{[]int{3, 4, 5}, sum, make(chan int)} // Send request clientRequests <- request // Wait for response. fmt.Printf("answer: %d\n", <-request.resultChan)
而在服务器端,只有处理句柄需要修改。
func handle(queue chan *Request) { for req := range queue { req.resultChan <- req.f(req.args) } }
要使其实际可用还有很多工作要做,这些代码仅能实现一个速率有限、并行、非阻塞RPC系统的
框架,而且它并不包含互斥量。
并行化
这些设计的另一个应用是在多CPU核心上实现并行计算。如果计算过程能够被分为几块
可独立执行的过程,它就可以在每块计算结束时向信道发送信号,从而实现并行处理。
让我们假设这个理想化的例子,
我们在对一系列向量项进行极耗资源的处理,而每个项的值计算是完全独立的。
type Vector []float64 // Apply the operation to v[i], v[i+1] ... up to v[n-1]. func (v Vector) DoSome(i, n int, u Vector, c chan int) { for ; i < n; i++ { v[i] += u.Op(v[i]) } c <- 1 // signal that this piece is done }
我们在循环中启动了独立的处理块,每个CPU将执行一个处理。
它们有可能以乱序的形式完成并结束,但这没有关系;我们只需要在所有
goroutine开始后接收并统计信道中的完成信号即可。
const NCPU = 4 // number of CPU cores func (v Vector) DoAll(u Vector) { c := make(chan int, NCPU) // Buffering optional but sensible. for i := 0; i < NCPU; i++ { go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c) } // Drain the channel. for i := 0; i < NCPU; i++ { <-c // wait for one task to complete } // All done. }
然而,目前 Go 运行时的实现默认在并不会并行执行。它为用户层代码使用单一处理核心。
任意数量的goroutine都可能在系统调用时被阻塞,但默认任意时候只有一个会执行用户层的代码。
它应该变得更智能,而且它将来会变得更智能的,但现在,如果你希望CPU并行执行你必须告诉运行时(runtime)
你希望有多少的goroutine能够同步执行。
有两种途径可以实行,要么将环境变量 GOMAXPROCS
的值设为核心数的值,要么导入 runtime
包
,并调用 runtime.GOMAXPROCS(NCPU)
.
runtime.NumCPU()
可能非常有用,它会返回当前机器的物理CPU数。
当然,随着调度及运行时的改进,将来可能不再需要采用这些方法。
一个可能出现内存泄露的缓冲区
并发编程的工具将同样使得非并发思想容易被实现。这里有一个从RPC包中提取的例子。
客户端从某些源,有可能是网络中循环接收数据。为了避免分配/释放缓冲区,它保存了一个
空闲链表,使用一个带缓冲区的信道表示。如果信道是空的,一个新的缓冲区将被分配。
一旦消息缓冲区就绪,它将被通过 serverChan
发送到服务器。
var freeList = make(chan *Buffer, 100) var serverChan = make(chan *Buffer) func client() { for { var b *Buffer // Grab a buffer if available; allocate if not. select { case b = <-freeList: // Got one; nothing more to do. default: // None free, so allocate a new one. b = new(Buffer) } load(b) // Read next message from the net. serverChan <- b // Send to server. } }
服务器从客户端循环接收每个消息,处理它们,并将缓冲区返回给空闲链表。
func server() { for { b := <-serverChan // Wait for work. process(b) // Reuse buffer if there's room. select { case freeList <- b: // Buffer on free list; nothing more to do. default: // Free list full, just carry on. } } }
客户端试图从 freeList
中获取缓冲区;如果没有缓冲区可用,它将分配一个
新的缓冲区。
服务器端通过向 freeList
发送,将 b
返回到空闲链表直到链表已满,
这种情况下,缓冲区将被丢弃,并且被垃圾回收器回收。
( select
语句中的 default
子句在没有条件符合时执行。)
这个实现使用短短几行建立了一个依赖于缓冲信道以及垃圾回收器来记录的,可能导致缓冲区
槽位泄露的空闲链表的实现。
错误
库例程通常需要返回某种类型的错误提示给调用者。之前提到过,Go 语言的多值返回特性
使得其在返回常规的返回值时,还能较轻松地返回详细的错误描述。根据约定俗成,错误的类型通常为
error
,这是一个内建的接口。
type error interface { Error() string }
库的编写者通过更丰富的底层模型可以轻松实现这个接口,这样不仅能看见错误,
还能提供一些上下文。
例如, os.Open
可以返回一个 os.PathError
.
// PathError records an error and the operation and // file path that caused it. type PathError struct { Op string // "open", "unlink", etc. Path string // The associated file. Err error // Returned by the system call. } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
PathError
的 Error
会输出如下错误信息:
open /etc/passwx: no such file or directory
这种错误包含了出错文件名、操作和触发的操作系统错误,在调用者在多级嵌套调用之上
打印该错误也是非常有用的,它比苍白的“不存在该文件或目录”更具说明性。
错误字符串应尽可能地包含它们的来源,例如包含产生该错误的包名的前缀。
例如,在 image
包中,由于未知格式导致解码错误的字符串为“image: unknown format”。
如果调用者关心错误的完整细节,可以使用类型转换或者类型断言来过滤特定错误,并抽取其细节。
对于 PathError
,它应该还包含检查内部的 Err
字段以进行可能的错误恢复。
for try := 0; try < 2; try++ { file, err = os.Create(filename) if err == nil { return } if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC { deleteTempFiles() // Recover some space. continue } return }
这里,第二条 if
语句属于 Go 的惯用法。这类类型断言 err.(*os.PathError)
通过惯用法“逗号 ok”来检查(我们在之前 检查映射的内容时提到过)
如果类型断言失败, ok
的值为假,而且 e
的值将为 nil
。
如果成功, ok
的值为真,这意味着这个错误属于 *os.PathError
类型,
e
能够获取更多关于该错误的信息。
Panic
报告错误给调用者的通常办法就是将 error
作为额外的返回值。
Read
方法就是一个典型的实例;它返回字节数和 error
。
但如果错误是不可恢复的呢?有时候程序就是不能够继续运行。
为此,我们提供了内建的 panic
函数,它将产生一个运行时错误并终止程序运行。
(但请继续看下一节)。该函数接受一个任意类型的参数—通常是一个字符串—并在程序结束
时打印。这还是一个表明某些不可能发生的事情已经发生了的方法,可能是从一个无限循环中退出了。
事实上,编译器在函数结束时将识别到 panic
,并且将跳过常规的 return
语句的检查。
// A toy implementation of cube root using Newton's method. func CubeRoot(x float64) float64 { z := x/3 // Arbitrary initial value for i := 0; i < 1e6; i++ { prevz := z z -= (z*z*z-x) / (3*z*z) if veryClose(z, prevz) { return z } } // A million iterations has not converged; something is wrong. panic(fmt.Sprintf("CubeRoot(%g) did not converge", x)) }
这仅仅只是一个示例,但实际的库函数应该避免 panic
。
如果问题可以被屏蔽或者被解决,最好是让程序继续运行而不是终止整个程序。
一个反例是初始化过程:如果库不能正确设置自身环境,且有足够的理由导致恐慌,那就让它恐慌吧。
var user = os.Getenv("USER") func init() { if user == "" { panic("no value for $USER") } }
Recover
当 panic
被调用时,包括不明确的运行时错误,例如数组的索引异常或者
是类型断言失败时,程序将立即结束当前函数的执行,并且回溯goroutine的库,并调用所有
推迟执行的函数。如果回溯至goroutine的栈顶,程序将中止。然而,可以使用内建的 recover
函数重新获得goroutine的控制权,并且使其恢复正常执行。
调用 recover
将停止回溯过程并且返回传递给 panic
的参数。
因为在回溯过程中只有被推迟的函数在运行,因此 recover
只能在被推迟的函数中才有效。
recover
的一个应用是在服务器中终止失败的goroutine而无需杀掉所有其它
正在执行的goroutine。
func server(workChan <-chan *Work) { for work := range workChan { go safelyDo(work) } } func safelyDo(work *Work) { defer func() { if err := recover(); err != nil { log.Println("work failed:", err) } }() do(work) }
在这个例子中,如果 do(work)
触发了panic,结果会被记录,而该goroutine会
正常的退出而不干扰其它。完全没有必要在推迟的闭包中执行其它任何操作,调用
recover
会处理好这一切。
因为仅被在推迟的函数中被调用时, recover
才不会返回 nil
,被推迟的代码
能够调用实现了 panic
和 recover
的库例程而不导致失败。
例如在 safelyDo
中的被延期函数可能在 recover
被调用前先调用记录函数,
而记录函数不应该受恐慌状态的影响。
通过恰当地使用恢复模式, do
(以及任何它将执行的代码)能够
通过调用 panic
而避免任何更坏的结果。
我们可以利用该思路在复杂的软件中简化出错处理。让我们看看从 regexp
包中
摘录的一个理想化的例子,它将在解析错误时以局部错误类型调用 panic
。
这是 Error
, error
方法和 Compile
函数的定义。
// Error is the type of a parse error; it satisfies the error interface. type Error string func (e Error) Error() string { return string(e) } // error is a method of *Regexp that reports parsing errors by // panicking with an Error. func (regexp *Regexp) error(err string) { panic(Error(err)) } // Compile returns a parsed representation of the regular expression. func Compile(str string) (regexp *Regexp, err error) { regexp = new(Regexp) // doParse will panic if there is a parse error. defer func() { if e := recover(); e != nil { regexp = nil // Clear return value. err = e.(Error) // Will re-panic if not a parse error. } }() return regexp.doParse(str), nil }
如果 doParse
触发了panic,恢复代码将会设置返回值为 nil
—
被延期函数能够修改命名的返回值。在 err
赋值过程中,将通过局部类型 Error
的类型断言来检查。
如果不属于解析错误,类型断言将失败,并触发一个运行时错误,并继续栈的回溯仿佛没有任何事情
中断一样。这段检查意味着如果一些异常发生了,例如数组索引越界,尽管我们使用
panic
和 recover
来处理用户触发的错误,代码仍将失败。
通过适时的处理错误, error
方法使得能够报告解析错误而无需
手动处理解析的栈回溯。
尽管这种模式很有用,它应该仅被用于包内。
Parse
将内部的 panic
调用转为 error
值;
它并不像调用者报告 panics
。这是一个应该遵守的良好准则。
顺便提一句,重新触发panic的惯用法会在实际错误发生时改变panic的值。然而,不论是
原始的或是新近的错误都将在崩溃报告中显示。因此,这类简单的再恐慌模型是够用
的—它毕竟只是一次崩溃—但如果你只想显示初始的错误值,你可以编写一些代码
来过滤不需要的异常并且使用初始值触发再恐慌。
就将这个作为练习留给读者吧。
一个小型web服务器
让我们以一个完整的 Go 程序作为结束吧,一个web服务器。
该程序实际上只是一种web服务器的重用。
Google在http://chart.apis.google.com
提供了一个自动将格式化数据转化为图表的服务。然而,该服务的交互性并不友好,
因为你需要将数据放入URL中作为查询。
我们的程序提供了数据形式的更好的接口:给定一小段文本,它将调用图表服务器并生成一个二维码(QR code),这是一个
对文本进行编码的矩阵框。
此图像可以通过你的手机摄像头获取,并且被解释为一个URL,为你免去了在狭窄的手机键盘上输入URL的麻烦。
以下是完整的程序。
随后有一段解释。
package main import ( "flag" "html/template" "log" "net/http" ) var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18 var templ = template.Must(template.New("qr").Parse(templateStr)) func main() { flag.Parse() http.Handle("/", http.HandlerFunc(QR)) err := http.ListenAndServe(*addr, nil) if err != nil { log.Fatal("ListenAndServe:", err) } } func QR(w http.ResponseWriter, req *http.Request) { templ.Execute(w, req.FormValue("s")) } // 此处为模版代码,由于md缘故被转义成了QR,sorry,知道如何解决此问题的朋友还请告知一声。 const templateStr = `QR Link Generator {{if .}}{{.}} {{end}}
`
在 main
之前的部分应该比较容易胜利街。我们通过一个标志设置了服务器默认的HTTP端口。
模板变量 templ
其乐无穷。它构建了一个将会被服务器执行并用于显示页面的HTML模板;稍后将详细讨论。
main
函数解析了参数标志,并使用我们讨论过的机制将 QR
函数绑定到服务
器的根路径。然后调用 http.ListenAndServe
启动服务器;它将在服务器运行时处于阻塞状态。
QR
仅接受包含表单数据的请求,并以表单值中的数据 s
执行模板。
模板包 html/template
非常强大;该程序只是浅尝辄止。
本质上,它通过在运行时将从数据项中提取数据(在这里是表单值)并传给 templ.Execute
执行而重写了HTML的文本。
在模板文本中( templateStr
),双大括号界定的文本表示模板的动作。
从 {{html "{{if .}}"}}
到 {{html "{{end}}"}}
的代码段仅在当前数据
项(这里是点 .
)不为空时才会执行。
也就是说,当字符串为空时,此部分模板段会被忽略。
其中两段 {{html "{{.}}"}}
表示要将数据显示在模板中——将查询字符串显示在web页面上。
HTML模板包将自动对文本进行转义因此文本显示是安全的。
余下的模板字符串只是页面加载时将要显示的HTML。
如果这段解释你无法理解,参考 文档 获得更多关于模板包的解释。
你终于如愿以偿了:以几行代码实现的包含一些数据驱动的HTML文本的web服务器。
Go 语言是一个强大到能让很多事情以短小精悍的方式解决的语言。
原文连接: Effective Go