Go程序设计语言(一)

本文译自Rob PikeGo语言PPT教程 – "The Go Programming Language Part 1(updated June 2011)"。由于该教程的最新更新时间早于Go 1版本发布,因此该PPT中的一些内容与Go 1语言规范略有差异,到时我会在相应的地方做上注解。

谁发明了Go

Go语言的设计和实现工作是由Google的一个研发小组以及来自世界各地的大量贡献者共同完成的。

联系方式:

课程大纲

第一部分:基础
第二部分:类型(type)、方法(method)以及接口(interface)
第三部分:并发(concurrency)与通信(communication)

这个课程是关于Go程序设计语言的,而不是关于编程语言设计方法的,后者是一个单独的话题,不在此教程范围内。

第一部分大纲

  • 动机
  • 基础 - 简单、熟悉的内容
  • 包与程序构建

动机

为什么要发明一门新语言?

在当今世界,编程语言在某些方面不够给力:

  • 计算机运行速度快,但软件的构建慢。
  • 为了速度和安全性需进行必要的依赖分析。
  • 在类型上遇到了太多的阻碍。
  • 对垃圾收集以及并发的支持太差。
  • 多核被视为危机而不是机会。

积极应对

我们的目标是让编程重新回归快乐。

  • 兼有动态语言的感觉以及静态类型系统的安全性;
  • 编译成机器语言以获得更快的运行速度;
  • 真正在运行时支持GC(垃圾收集)以及并发;
  • 轻量级、灵活的类型系统;
  • 拥有方法(method),但却不是传统的OO(面向对象)语言。

资源

关于Go语言的更多背景资料请参见文档:http://golang.org

文档包括:

  • 语言规范
  • 教程
  • "Effective Go"
  • 标准库文档
  • 安装和How-to文档
  • FAQs
  • 一个语言联系游乐场(在浏览器中运行Go程序)
  • 更多

现状:编译器

gc(Ken Thompson),又称6g,8g,5g
        继承自Plan 9项目的编译器模型
        生成代码速度非常快
        不支持gcc直接链接

gccgo(Ian Taylor)
        更为熟悉的体系架构
        生成代码的速度没有gc那样快
        支持gcc直接链接

支持32-bit和64-bit x86 (amd64,x86-64) 以及ARM。

垃圾收集器,并发等都已实现。
优秀且正逐步完善的标准库。

基础

是时候上一些代码了

package main

import "fmt"
 
func main() {
    fmt.Print("Hello, 世界\n")
}
 

语言基础

  • 假设熟悉其他C语言类的(C-like)编程语言,这里将快速浏览一些基础知识。
  • 这里大部分内容是简单的且熟悉的,也可能因此而有些沉闷,这里先说声道歉。
  • 接下来的两部分教程会包含很有趣的内容,不过我们首先需要打下良好基础。

词法结构

- 传统中蕴含新意。
- 源码采用UTF-8编码。空格包括:空白,tab,换行,回车。
- 标识符由字母和数字组成(外加'_'),字母和数字都是Unicode编码。
- 注释:
/* This is a comment; no nesting */
// So is this.
 

字面值(literals)

类似C语言中的字面值,但数值不需要符号以及大小标志(后续会有更多这方面内容):
 
23
0x0FF
1.234e7
 
类似C中的字符串,但字符串是Unicode/UTF-8编码的。同时,\xNN总是有2个数字;\012总是3;两个都是字节:
 
"Hello, world\n"
"\xFF"       // 1 byte
"\u00FF"     // 1 Unicode char, 2 bytes of UTF-8
 
原生字符串:`\n\.abc\t\` == "\\n\\.abc\\t\\"

语法概述

基本上就是类C的语法,但使用反转的类型和声明,并使用关键字作为每个声明的开头。
 
var a int
var b, c *int // 注意与C的不同
var d []int
type S struct { a, b int }
 
基本的控制结构也十分熟悉:
 
if a == b { return true } else { return false }
for i = 0; i < 10; i++ { … }
 
注意:没有圆括号,但需要大括号。
 
后续会有更多有关这方面的内容。
 

分号

分号作为语句终止符号,但:
- 如果前一个符号是语句的结尾,那词法分析程序将自动在行尾插入一个分号
- 注意:比JavaScript的规则更清晰和简单

因此,下面的程序不需要分号:

package main

const three = 3
var i int = three

func main() { fmt.Printf("%d\n", i) }

在实际中,Go源码在for和if子句之外几乎都没有用到分号。

数值类型

数值类型(numeric types)是原生内置的,也是为大家所熟知的:

int          uint
int8      uint8 = byte
int16       uint16
int32       uint32         float32      complex64
int64       uint64         float64      complex128

还有uintptr,一个大小足够存储一个指针的数值。

这些都是互不相同的类型;int不等于是int32,即便是在一个32位的机器上。

没有隐式类型转换(不过不要恐慌)。

Bool

普通的布尔类型bool,取值true和false(预定义的常量)。

if语句等使用布尔表达式。

指针类型和整型不是布尔类型。

string

原生内置的string类型代表不可改变的字节数组,即文本。string类型是用长度定界的,而不是以结尾0终止的。

字符串字面值是string类型。

和整型一样不可改变。可重新赋值,但不能修改其值。

正如"3"总是3,"hello"也总是"hello"。

Go语言对字符串操作提供了良好的支持。

表达式(Expressions)

大多都是类C语言的操作符。

二元操作符:

优先级                操作符                    备注

5                 * / % << >> & &^         &^是位清理操作符
4                 + – | ^                  ^是异或(xor)

3                 == != < <= > >=
2                 &&
1                 ||

一元操作符包括:& ! * + – ^(外加用于通信的<-)
一元操作符^是求补码/反码操作。

Go vs. C表达式

可以让C程序员惊喜的是:

更少的优先级层次(应该容易)。
^替代了~
++和–不再是表达式操作符(x++是一个语句,不是表达式;*p++是(*p)++,而不是*(p++))
&^是新操作符,在常量表达式中很有用
<<和>>等需要一个无符号的移位计数。

无惊喜的是:

赋值操作与所期望的一样:+= <<= &^=等
表达式总体看起来相似(下标、函数调用等)

例子

+x
23 + 3*x[i]
x <= f()
^a >> b
f() || g()
x == y + 1 && <-ch > 0
x &^ 7 // x with the low 3 bits cleared
fmt.Printf("%5.2g\n", 2*math.Sin(PI/8))
7.234/x + 2.3i

"hello, " + "world"  // concatenation
                     // no C-like "a" "b"

数值转型

将一个数值从一个类型转换为另一个类型称为一次转型,其语法形式有点类似函数调用:

uint8(intVar)   //截断到相应的大小
int(float64Var) //片段截断
float64(intVar) //转为float64

一些涉及string类型的转型:

string(0×1234)          // == "\u1234"
string(sliceOfBytes)    // bytes -> bytes
string(sliceOfInts)     // ints -> Unicode/UTF-8
[]byte("abc")           // bytes -> bytes
[]int("日本語")          // Unicode/UTF-8 -> ints

切片(slice)与数组相关,稍后会有更多相关内容。

常量

数值常量是"理想数":没有大小或标志,因此没有U、L或UL作结尾。

077 // 八进制
0xFEEDBEEEEEEEEEEEEEEEEEEEEF //十六进制
1 << 100

下面是整数和浮点数值,字面值的语法决定其类型:

1.234e5    // 浮点
1e2        // 浮点
3.2i       // 浮点虚数
100        // 整数

常量表达式

浮点和整型常量可以任意组合,最终表达式的类型由常量的类型决定。操作自身也取决于类型。

2*3.14   // 浮点: 6.28
3./2     // 浮点:1.5
3/2      // 整型:1
3+2i     // 复数:3.0 + 2.0i

// 高精度
const Ln2 = 0.69314718055994530941723212145817656807
const Log2E = 1/Ln2 

数值的表示范围足够大(目前最大用1024位表示)。

理想数的结果

Go语言允许无需显式转型的情况下使用常量,前提是常量值可以被其类型表示(没有必要进行转型;其值表示起来没问题):

var million int = 1e6  //float语法在这里可以使用
math.Sin(1)

常量必须可以被其类新表示。例如:^0的值为-1,不在0-255的范围内。

uint8(^0)       //错误:-1无法用uint8类型表示
^uint8(0)       //OK
uint8(350)      //错误:350无法用uint8类型表示
uint8(35.0)     //OK: 35
uint8(3.5)      //错误:3.5无法用uint8类型表示

声明

声明以一个关键字开头(var, const,type和func),并且与C中的声明次序相反:

var i int
const PI = 22./7.
type Point struct { x, y int }
func sum(a, b int) int { return a + b }

为何要以相反次序声明呢?早期的一个例子:

var p, q *int

p和q的类型都是*int。并且函数读起来更佳,并且与其他声明一致。还有一个原因,马上道来。

Var

变量声明以var开头。

它们可以有一个类型或一个初始化表达式;至少应有一个或二者都有。初始化表达式应该与变量匹配(还有类型!)。

var i int
var j = 365.245
var k int = 0
var l, m uint64 = 1, 2
var nanoseconds int64 = 1e9 // float64 constant!
var inter, floater, stringer = 1, 2.0, "hi"

分派var

总是输入var让人生厌。我们可以通过括号让多个变量声明成为一组:

var (
    i int
    j = 356.245
    k int = 0
    l, m uint64 = 1, 2
    nanoseconds int64 = 1e9
    inter, floater, stringer = 1, 2.0, "hi"
)

这种形式适用于const,type, var,但不能用于func。

=:"短声明"

在函数内(只有在函数内这一种情况下),下面形式的声明:
  var v = value

可以被缩短成:
  v := value

(这就是另外一个名字、类型倒序的原因)

类型就是值的类型(对于理想数,相应的类型是int或float64或complex128)
  a, b, c, d, e := 1, 2.0, "three", FOUR, 5e0i

这种形式的声明使用很频繁,并且在诸如for循环初始化表达式中也可以使用。

Const

常量声明以const开头。

它们必须有一个常量表达式,可在编译期间求值,作为初始化表达式,可以拥有一个可选的类型修饰符。

const Pi = 22./7.
const AccuratePi float64 = 355./113
const beef, two, parsnip = "meat", 2, "veg"
const (
    Monday, Tuesday, Wednesday = 1, 2, 3
    Thursday, Friday, Saturday = 4, 5, 6
)

Iota

常量声明可以使用计数器:iota,每个const块中的iota都从0开始计数,在每个隐式的分号(行尾)自增。

const (
    Monday = iota  // 0
    Tuesday = iota // 1
)

速记:重复上一个类型和表达式。

const (
    loc0, bit0 uint32 = iota, 1<<iota //0,1
    loc1, bit1                        //1,2
    loc2, bit2                        //2,4
)

Type

类型声明以type开头。

我们后续会学习更多类型,不过先这里举几个例子:

type Point struct {
    x, y, z float64
    name
    string
}
type Operator func(a, b int) int
type SliceOfIntPointers []*int

我们稍后会回到函数。

New

内置函数new用于分配内存。其语法类似一个函数调用,以类型作为参数,与C++中的new类似。返回一个指向已分配对象的指针。

var p *Point = new(Point)
v := new(int)   // v的类型为*int

稍后我们将看到如何构建切片(slice)

Go语言中没有用于内存释放的delete或free。Go具备垃圾回收功能。

赋值

赋值是容易和熟悉的:

a = b

但Go还支持多项赋值:

x, y, z = f1(), f2(), f3()
a, b = b, a  //交互a,b的值

函数支持多个返回值(稍后有更多细节):

nbytes, error := Write(buf)

控制结构

与C类似,但很多地方有不同。

Go支持if、for和switch。

正如之前说的,无需小括号,但大括号是必要的。

如果将它们看为一组,它们的用法很规律。例如,if、for和switch都支持初始化语句。

控制结构的形式

后续会有细节,但总体上:

if和switch语句以1元素和2元素形式呈现,后面详细讲解。

for循环具有1元素和3元素的形式:

1元素形式等价于C语言中的while:
    for a {}
3元素形式等价于C语言中的for:
    for a;b;c {}

在所有这些形式里,任何元素都可以是空。

if

基本形式是大家所熟知的,但已经没有了"else悬挂"问题了:

if x < 5 { less() }
if x < 5 { less() } else if x == 5 { equal() }

支持初始化语句;需要分号。

if v := f(); v < 10 {
    fmt.Printf("%d less than 10\n", v)
} else {
    fmt.Printf("%d not less than 10\n", v)
}

与多元函数一起使用更有益处:

if n, err = fd.Write(buf); err != nil { … }

省略条件意为true,在这里没有什么用。但在for,switch语句中尤其有用。

for

基本形式是大家所熟知的:
    for i := 0; i < 10; i++ { … }

省略条件意为true:

    for ;; { fmt.Printf("looping forever") }

而且你还可以省略分号:
   
    for { fmt.Printf("Mine! ") }

不要忘记多项赋值:
    for i,j := 0,N; i < j; i,j = i+1,j-1 {…}

(Go中没有像C中那样的逗号操作符)

switch细节

switch与C中的switch有些类似。

不过,有一些语法和语义的重要不同之处:
- 表达式不必一定是常量,甚至可以不必是int。
- 没有自动的fall through
- 但作为替代,语法上,最后的语句可以为fallthrough
- 多case可以用逗号分隔

switch count%7 {
    case 4,5,6: error()
    case 3: a *= v; fallthrough
    case 2: a *= v; fallthrough
    case 1: a *= v; fallthrough
    case 0: return a*v
}

Switch

Go中的switch要远比C中的强大。常见的形式:

switch a {
    case 0: fmt.Printf("0")
    default: fmt.Printf("non-zero")
}

switch表达式可以是任意类型,如果为空,则表示true。结果类似一个if-else链:

a, b := x[i], y[j]
switch {
    case a < b: return -1
    case a == b: return 0
    case a > b: return 1
}

switch a, b := x[i], y[j]; { … }

Break,continue等

break和continue语句的工作方式与C中的类似。

它们可以指定一个label并影响外层结构:

Loop: for i := 0; i < 10; i++ {
    switch f(i) {
        case 0, 1, 2: break Loop
    }
    g(i)
}

是的,那是一个goto。

函数

函数以func关键字开头。

如果有返回类型,返回类型放在参数的后面。return的含义和你期望的一致。

func square(f float64) float64 { return f*f }

函数支持返回多个值。这样,返回类型就是一个括号包围的列表。

func MySqrt(f float64) (float64, bool) {
    if f >= 0 { return math.Sqrt(f), true }
    return 0, false
}

空标识符

如果你只关心MySqrt函数返回的第一个值?你仍然需要将第二个值放在一个地方。

解决方法:使用空标识符_(下划线)。它是预声明的,可以被赋予任何无用的值。

// Don't care about boolean from MySqrt.
val, _ = MySqrt(foo())

在空标识符其他的适用场合中,我们仍然会展示它。

带结果变量(result variable)的函数

如果你给结果参数命名了,你可以将它当作实际变量使用。

func MySqrt(f float64) (v float64, ok bool) {
    if f >= 0 { v,ok = math.Sqrt(f), true }
    else { v,ok = 0,false }
    return v,ok
}

结果变量被初始化为"0"(0,0.0,false等。根据其类型;稍后有更多有关内容)

func MySqrt(f float64) (v float64, ok bool) {
    if f >= 0 { v,ok = math.Sqrt(f), true }
    return v,ok
}

空返回

最后,一个没有返回表达式的return将返回结果变量的当前值。下面是另外两个MySqrt的版本:

func MySqrt(f float64) (v float64, ok bool) {
    if f >= 0 { v,ok = math.Sqrt(f), true }
    return // must be explicit
}
func MySqrt(f float64) (v float64, ok bool) {
    if f < 0 { return } // error case
    return math.Sqrt(f),true
}

0是什么

Go中的内存都是被初始化了的。所有变量在执行之前的声明时被初始化。如果没有显式的初始化表达式,我们将使用对应类型的"0值"。下面的循环:

for i := 0; i < 5; i++ {
    var v int
    fmt.Printf("%d ", v)
    v = 5
}
将打印0 0 0 0 0。

0值取决于类型:数值是0;布尔是false;空字符串是"";指针,map、切片、channel是nil;结构体是0等。

Defer

defer语句负责在其所在的函数返回时执行一个函数(或方法)。其参数在到达defer语句那个时刻被求值;其函数在返回时被执行。

func data(fileName string) string {
    f := os.Open(fileName)
    defer f.Close()
    contents := io.ReadAll(f)
    return contents
}

在关闭文件描述符、解互斥锁等场合十分有用。

每Defer执行一个函数

Go按按后入先出(LIFO)次序执行一组defer函数。

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

上面代码将输出4 3 2 1 0。你可以在最后关闭所有文件描述符以及解锁所有互斥锁。

用defer跟踪代码

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

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

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

func main() { b() }

不过我们可以实现的更灵巧一些。

参数当即求值,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() }

函数字面值

和在C中一样,函数不能在函数内部声明。但函数字面值却可以被赋值给变量。

func f() {
    for i := 0; i < 10; i++ {
        g := func(i int) { fmt.Printf("%d",i) }
        g(i)
    }
}

函数字面值是闭包(closure)

函数字面值实际上是闭包。

func adder() (func(int) int) {
    var x int
    return func(delta int) int {
        x += delta
        return x
    }
}

f := adder()
fmt.Print(f(1))
fmt.Print(f(20))
fmt.Print(f(300))

输出1 21 321 – f中的x累加。

程序构建

包(package)

一个程序以一个包的形式构建,这个包还可以使用其他包提供的一些设施。

一个Go程序的创建是通过链接一组包。

一个包可以由多个源码文件组成。

导入包中的名字可以通过packagename.Itemname访问。

源码文件结构

每个源码文件包括:

- 一个package字句(文件归属于哪个包);其名字将作为导入包时的默认名字。
    package fmt
- 一个可选的import声明集
    import "fmt"  //使用默认名字
    import myFmt "fmt" //使用名字myFmt

- 0个或多个全局或“包级别”声明。

单一文件包

package main // 这个文件是包main的一部分

import "fmt" // 这个文件使用了包"fmt"

const hello = "Hello, 世界\n"

func main() {
    fmt.Print(hello)
}

main和main.main

每个Go程序包含一个名为main的包以及其main函数,在初始化后,程序从main开始执行。类似C,C++中的main()函数。

main.main函数没有参数,没有返回值。当main.main返回时,程序立即退出并返回成功。

os包

os包提供Exit函数以及访问文件I/O以及命令行参数的函数等。

// A version of echo(1)
package main

import (
    "fmt"
    "os"
)

func main() {
    if len(os.Args) < 2 { // length of argument slice
        os.Exit(1)
    }
    for i := 1; i < len(os.Args); i++ {
        fmt.Printf("arg %d: %s\n", i, os.Args[i])
    }
} // falling off end == os.Exit(0)

全局作用域与包作用域

在一个包中,所有全局变量、函数、类型以及常量对这个包的所有代码可见。

对于导入该包的包而言,只有以大写字母开头的名字是可见的:全局变量、函数、类型、常量以及方法和结构体中全局类型以及变量的字段。

const hello = "you smell"    // 包内可见
const Hello = "you smell nice"  //全局可见
const _Bye = "stinko!"        // _不是大写字母

这与C/C++非常不同:没有extern、static、private以及public。

初始化

有两种方法可以在main.main执行前初始化全局变量:
1) 带有初始化语句的全局声明
2) 在init函数内部,每个源文件中都可能有init函数。

包依赖可以保证正确的执行顺序。

初始化总是单线程的。

初始化例子

package transcendental

import "math"

var Pi float64

func init() {
    Pi = 4*math.Atan(1) // init function computes Pi
}
====
package main

import (
    "fmt"
    "transcendental"
)

var twoPi = 2*transcendental.Pi // decl computes twoPi

func main() {
    fmt.Printf("2*Pi = %g\n", twoPi)
}
====
输出: 2*Pi = 6.283185307179586

包与程序构建

要构建一个程序,包以及其中的文件必须按正确的次序进行编译。包依赖关系决定了按何种次序构建包。

在一个包内部,源文件必须一起被编译。包作为一个单元被编译,按惯例,每个目录包含一个包,忽略测试,

cd mypackage
6g *.go

通常,我们使用make; Go语言专用工具即将发布(译注:Go 1中可直接使用go build、go install等高级命令,可不再直接用6g、6l等命令了。)

构建fmt包

% pwd
/Users/r/go/src/pkg/fmt
% ls
Makefile fmt_test.go format.go print.go # …
% make # hand-written but trivial
% ls
Makefile _go_.6 _obj fmt_test.go format.go print.go # …
% make clean; make

目标文件被放在_obj子目录中。

编写Makefiles时通常使用Make.pkg提供的帮助。看源码。

测试

要测试一个包,可在这个包内编写一组Go源文件;给这些文件命名为*_test.go。

在这些文件内,名字以Test[^a-z]开头的全局函数会被测试工具gotest自动执行,这些函数应使用下面函数签名:

func TestXxx(t *testing.T)

testing包提供日志、benchmarking、错误报告等支持。

一个测试例子

摘自fmt_test.go中的一段有趣代码:

import (
    "testing"
)

func TestFlagParser(t *testing.T) {
    var flagprinter flagPrinter
    for i := 0; i < len(flagtests); i++ {
        tt := flagtests[i]
        s := Sprintf(tt.in, &flagprinter)
        if s != tt.out {
            // method call coming up – obvious syntax.
            t.Errorf("Sprintf(%q, &flagprinter) => %q,"+" want %q", tt.in, s, tt.out)
        }
    }
}

gotest(译注:在go 1中gotest工具用go test命令替代)

% ls
Makefile fmt.a fmt_test.go format.go print.go # …
% gotest # by default, does all *_test.go
PASS
wally=% gotest -v fmt_test.go
=== RUN fmt.TestFlagParser
— PASS: fmt.TestFlagParser (0.00 seconds)
=== RUN fmt.TestArrayPrinter
— PASS: fmt.TestArrayPrinter (0.00 seconds)
=== RUN fmt.TestFmtInterface
— PASS: fmt.TestFmtInterface (0.00 seconds)
=== RUN fmt.TestStructPrinter
— PASS: fmt.TestStructPrinter (0.00 seconds)
=== RUN fmt.TestSprintf
— PASS: fmt.TestSprintf (0.00 seconds) # plus lots more
PASS
%

一个benchmark的测试例子

Benchmark的函数签名如下:

func BenchmarkXxxx(b *testing.B)

并被循环执行b.N次;其余的由testing包完成。

下面是一个来自fmt_test.go中的benchmark例子:

package fmt // package is fmt, not main
import (
    "testing"
)
func BenchmarkSprintfInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Sprintf("%d", 5)
    }
}

Benchmarking: gotest

% gotest -bench="." # regular expression identifies which
fmt_test.BenchmarkSprintfEmpty 5000000
310 ns/op
fmt_test.BenchmarkSprintfString 2000000
774 ns/op
fmt_test.BenchmarkSprintfInt
5000000
663 ns/op
fmt_test.BenchmarkSprintfIntInt 2000000
969 ns/op

%

库就是包。

目前的库规模是适中的,但还在增长。

一些例子:

包                目的             例子
fmt            格式化I/O           Printf、Scanf
os             OS接口              Open, Read, Write
strconv        numbers<-> strings  Atoi, Atof, Itoa
io             通用I/O             Copy, Pipe
flag           flags: –help等     Bool, String
log            事件日志             Logger, Printf
regexp         正则表达式           Compile, Match
template       html等              Parse, Execute
bytes          字节数组             Compare, Buffer

更多关于fmt

fmt包包含一些熟悉的名字:

Printf – 打印到标准输出
Sprintf – 返回一个字符串
Fprintf – 写到os.Stderr等

还有

Print, Sprint, Fprint – 无格式no format
Println, Sprintln, Fprintln – 无格式,但中间加入空格,结尾加入\n

fmt.Printf("%d %d %g\n", 1, 2, 3.5)
fmt.Print(1, " ", 2, " ", 3.5, "\n")
fmt.Println(1, 2, 3.5)

每个都输出相同的结果:"1 2 3.5\n"

库文档

源码中包含注释。

命令行或web工具可以将注释提取出来。

链接:http://golang.org/pkg/

命令:
    % godoc fmt
    % godoc fmt Printf

也谈Go语言编程 – Hello,Go!

Go is expressive, concise, clean, and efficient. Its concurrency mechanisms make it easy to write programs that get the most out of multicore and networked machines, while its novel type system enables flexible and modular program construction. Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It's a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.
                                                                                          – 摘自Go语言官方站点

对于一门编程语言最深刻的喜欢莫过于对这门编程语言的设计理念的认同了,Go语言是继C语言之后又一门让我有如此感觉的编程语言。

初听到这门语言的存在时,我皱了皱眉:怎么起了这么一个名字!绝大多数编程语言都以名词或人名命名(如C、Java、PythonRubyHaskell、Ada等),而这门语言却用了一个日常生活中使用极为频繁的动词Go作为名字,这似乎有些太大众化了。不知为何,这个名字总是让 我联想到以前中国农村给小孩子常起的几个名字:二狗、牛娃等^_^。况且之前已经有很多IT产品也以Go作为名字(例 如,Thoughtworks公司出品的敏捷管理工具也叫Go)。

不过随着对这门语言的了解的深入,名字已不再是问题了。Go语言对我这个C程序员产生了强大的吸引力,原因如下:

* Go保持了与C语言一脉相承的理念:短小精悍、致力于成为系统编程语言、简洁而熟悉的C语言家族语法、静态编译型语言、保留了指针、运行高效;
* Go填平了C语言与生俱来的为数不少的"坑";
* Go提升了编译速度,统一了源码组织、构建规范以及编码规范,让程序员更集中精力于问题域;
* Go改进了并发模型,在语言级别原生支持多核平台;
* Go语言起点高,以创新的设计以及甚小的代价兼容了现有主流编程范型(例如OO等)。

因此有人称Go为21世纪的C语言,我觉得不为过。从这篇文章开始,我将和大家一起走入Go语言的世界。

一、安装Go

Go语言官方站(从国内访问十分不稳定,时能时不能,原因你懂的)对Go安装有着较为详尽的说明。如果你使用的是Linux、Mac OS或Windows,那你应该可以很顺利地完成Go的安装。Go于今年上旬发布了第一个稳定版本Go 1,目前最新版本是1.0.2,可以从Google Code上的Go项目中下载。我的环境为Ubuntu 10.04 32-bit,下载go1.0.2.linux-386.tar.gz后,解压到/usr/local/go下面:

$ ls /usr/local/go
api/     bin/           doc/        include/  LICENSE  PATENTS    README        src/   VERSION
AUTHORS  CONTRIBUTORS  favicon.ico  lib/      misc/    pkg/    robots.txt  test/

然后将/usr/local/go/bin添加到你的PATH环境变量中,你就可以在任意目录下执行go程序了:

$ go version
go version go1.0.2

如果你得到上面的输出结果,可以断定你的Go安装成功了!

二、第一个Go程序 – Hello, Go!

我们建立一个用于编写Go程序的工作目录go-examples,其绝对路径为/home/tonybai/go-examples。好了,开始 编写我们的第一个Go程序。

我们在go-examples下创建一个文件hellogo.go,其内容如下:

package main

import (
    "fmt"
)

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

下面我们来编译该源文件并执行生成的可执行文件:

$ go build hellogo.go
$ ls
hellogo*  hellogo.go
$ hellogo
Hello, Go!

通过go build加上要编译的Go源文件名,我们即可得到一个可执行文件,默认情况下这个文件的名字为源文件名字去掉.go后缀。当然我们也 可以通过-o选项来指定其他名字:

$ go build -o myfirstgo hellogo.go
$ ls
myfirstgo*  hellogo.go

如果我们在go-examples目录下直接执行go build命令,后面不带文件名,我们将得到一个与目录名同名的可执行文件:

$ go build
$ ls
go-examples*  hellogo.go

三、程序入口点(entry point)和包(package)

Go保持了与C家族语言一致的风格:即目标为可执行程序的Go源码中务必要有一个名为main的函数,该函数即为可执行程序的入口点。除此之外 Go还增加了一个约束:作为入口点的main函数必须在名为main的package中。正如上面hellogo.go源文件中的那样,在源码第 一行就声明了该文件所归属的package为main。

Go去除了头文件的概念,而借鉴了很多主流语言都采用的package的源码组织方式。package是个逻辑概念,与文件没有一一对应的关系。 如果多个源文件都在开头声明自己属于某个名为foo的包,那这些源文件中的代码在逻辑上都归属于包foo(这些文件最好在同一个目录下,至少目前 的Go版本还无法支持不同目录下的源文件归属于同一个包)。

我们看到hellogo.go中import一个名为fmt的包,并利用该包内的Printf函数输出"Hello, Go!"。直觉告诉我们fmt包似乎是一个标准库中的包。没错,fmt包提供了格式化文本输出以及读取格式化输入的相关函数,与C中的printf或 scanf等类似。我们通过import语句将fmt包导入我们的源文件后就可以使用该fmt包导出(export)的功能函数了(比如 Printf)。

在C中,我们通过static来标识局部函数还是全局函数。而在Go中,包中的函数是否可以被外部调用,要看该函数名的首母是否为大写。这是一种 Go语言固化的约定:首母大写的函数被认为是导出的函数,可以被包之外的代码调用;而小写字母开头的函数则仅能在包内使用。在例子中你也看到了 fmt包的Printf函数其首母就是大写的。

四、GOPATH

我们把上面的hellogo.go稍作改造,拆分成两个文件:main.go和hello.go。

/* hello.go */
package hello

import "fmt"

func Hello(who string) {
    fmt.Printf("Hello, %s!\n", who)
}

/* main.go */
package main

import (
    "hello"
)

func main() {
    hello.Hello("Go!")
}

用go build编译main.go,结果如下:

$ go build main.go
main.go:4:2: import "hello": cannot find package

编译器居然提示无法找到hello这个package,而hello.go中明明定义了package hello了。这是怎么回事呢?原来go compiler搜索package的方式与我们常规理解的有不同,Go在这方面也有一套约定,这里面涉及到一个重要的环境变量:GOPATH。我们可以使用go help gopath来查看一下有关gopath的manual。

Go compiler的package搜索顺序是这样的,以搜索hello这个package为例:

* 首先,Go compiler会在GO安装目录(GOROOT,这里是/usr/local/go)下查找是否有src/pkg/hello相关包源码;如果没有则继续;
* 如果export GOPATH=PATH1:PAHT2,则Go compiler会依次查找是否存在PATH1/src/hello、PATH2/src/hello;配置在GOPATH中的PATH1和PATH2被称作workplace;
* 如果在上述几个位置均无法找到hello这个package,则提示出错。

在本例子中,我们尚未设置过GOPATH环境变量,也没有建立类似PATH1/src/hello这样的路径,因此Go compiler显然无法找到hello这个package了。我们来设置一下GOPATH变量并建立相关目录:

$ export GOPATH=/home/tonybai/go-examples
$ mkdir src/hello
$ mv hello.go src/hello
$ go build main.go
$ ls
main*  main.go    src/
$ main
Hello, Go!

五、Go install

我们将main.go移到src/main中,这样这个demo project显得更加合理,所有源码均在src下:

$cd src
$ ls
hello/    main/

Go提供了install命令,与build命令相比,install命令在编译源码后还会将可执行文件或库文件安装到约定的目录下。我们以main目录为例:

$ cd main
$ go install

install命令执行后,我们发现main目录下没有任何变化,原先build时产生的main可执行文件也不见了踪影。别急,前面说过Go install也有一套自己的约定:
* go install(在src/DIR下)编译出的可执行文件以其所在目录名(DIR)命名
* go install将可执行文件安装到与src同级别的bin目录下,bin目录由go install自动创建
* go install将可执行文件依赖的各种package编译后,放在与src同级别的pkg目录下

现在我们来看看bin目录:
$ ls /home/tonybai/go-examples
bin/  src/ pkg/
$ ls bin
main*

的确出现一个bin目录,并且刚刚编译的程序main在bin下面。

hello.go编译后并非可执行程序,在编译main的同时,由于main依赖hello package,因此hello也被关联编译了。这与单独在hello目录下执行install的结果是一样的,我们试试:

$ cd hello
$ go install
$ ls /home/tonybai/go-examples
bin/  pkg/  src/

在我们的workspace(go-examples目录)下出现了一个pkg目录,pkg目录下是一个名为linux_386的子目录,其下面有一个文 件:hello.a。这就是我们install的结果。hello.go被编译为hello.a并安装到pkg/linux_386目录下了。

.a这个后缀名让我们想起了静态共享库,但这里的.a却是Go独有的文件格式,与传统的静态共享库并不兼容。但Go语言的设计者使用这个后缀名似乎是希望 这个.a文件也承担起Go语言中"静态共享库"的角色。我们不妨来试试,看看这个hello.a是否可以被Go compiler当作"静态共享库"来对待。我们移除src中的hello目录,然后在main目录下执行go build:

$ go build
main.go:4:2: import "hello": cannot find package

Go编译器提示无法找到hello这个包,可见目前版本的Go编译器似乎不理pkg下的.a文件。http://code.google.com/p/go/issues/detail?id=2775 这个issue也印证了这一点,不过后续Go版本很可能会支持链接.a文件。毕竟我们在使用第三方package的时候,很可能无法得到其源码,并且在每个项目中都保存一份第三方包的源码也十分不利于项目源码的后期维护。

六、像脚本一样运行Go源码

Go具有很高的编译效率,这得益于其设计者对该目标的重视以及设计过程中细节方面的把控,当然这不是本文要关注的话题。正是由于go具有极速的编译,我们才可以像使用运行脚本语言那样使用它。

目前Go提供了run命令来直接运行源文件。比如:

$ go run main.go
Hello, Go!

go run实际上是一个将编译源码和运行编译后的二进制程序结合在一起的命令。但目前go源文件尚不支持作成Shebang Script,因为Go compiler尚不识别#!符号,下面的源码文件运行起来会出错:

#! /usr/local/go/bin/go run

package main

import (
    "hello"
)

func main() {
    hello.Hello("Go!")
}

$ go run main.go
package :
main.go:1:1: illegal character U+0023 '#'

不过我们可以可借助一些第三方工具来运行Shebang Go scripts,比如gorun

七、测试Go程序

前面说过Go起点较高,因此其自身就提供了一个轻量级单元测试框架包以及运行测试集的命令。

我们用一个例子来说明如何编写包的测试代码以及如何运行这个测试。我们在go-examples/src下建立另外一个目录mymath,mymath目录下mymath包的代码如下:

/* mymath.go */
package mymath

func MyAdd(i int, j int) int {
    return i + j
}

要对mymath包进行测试,我们需在同一目录下创建mymath_test.go文件,其中对MyAdd函数的测试代码如下:

/* mymath_test.go */
package mymath

import "testing"

func TestMyAdd(t *testing.T) {
    a, b := 4, 2
    if x := MyAdd(a, b); x != 6 {
        t.Errorf("MyAdd(%d, %d) = %d, want %d", a, b, x, 6)
    }
}

在这个文件中我们import了Go提供的标准单元测试包-testing,并且每个测试方法都已Test作为前缀开头。现在我们来运行一下这个测试,在mymath目录下运行go test命令:

$ go test
PASS
ok      mymath    0.007s

如果用例出错,我们就可看到下面提示:

$go test
— FAIL: TestMyAdd (0.00 seconds)
    mymath_test.go:8: MyAdd(4, 2) = 6, want 6
FAIL
exit status 1
FAIL    mymath    0.007s

由上可以看出,Go test也有自己的一些约定:测试源文件的名字必须以_test.go作为结尾;测试代码与被测代码在同一个包中;测试代码要导入testing包;测试 函数要以Test作为前缀,并且测试函数的函数签名必须是这样的:func TestXXX(t *testing.T)。

语言自带对测试的支持的好处是一致性,避免了大家使用不同的测试框架而给阅读、交流和维护带来的不便。

八、项目源码组织

有了源码、有了对编译原理的理解、有了测试框架的支持,我们就可以策划项目源码组织形式了。不过Go的诸多约定基本上已经将我们限制在如下结构上:

proj1/
    bin/
        myapp1*
    pkg/
        linux_386/
            lib1.a
            lib2.a
    src/
        lib1/
            lib1.go     
            lib1_test.go
        lib2/
            lib2.go     
            lib2_test.go
        … …
        myapp1/
            main.go       # main package source
            main_test.go  # test source

proj2/
    bin/
        myapp2*
    pkg/
        linux_386/
            lib3.a
            lib4.a
    src/
        lib3/
            lib3.go     
            lib3_test.go
        lib4/
            lib4.go     
            lib4_test.go
        … …
        myapp2/
            main.go       # main package source
            main_test.go  # test source

基于上述结构,我们可将GOPATH设置为proj1_path:proj2_path

九、代码风格(coding style)

Go程序员可以不再纠结于到底使用哪种代码风格,因为Go已经将代码风格做了严格的约定,一旦违反,Compiler直接给出Error。go还提供了fmt命令来协助Go程序员按标准格式化源文件。

从上面例子中我们可以看到Go的几大风格特点是:

* 左大括号'{'一定在函数名或if等语句在同一行
   func foo {

   }

* 无需显式用分号;将语句分隔(除非是在一行写上多条语句),因为compiler会替大家在适当位置加入分号的。
   i, j := 2, 3
   MyAdd(i, j)

   if x := MyAdd(a, b); x != 6 {
            … …
   }

* if、for等后面的表达式无需用小括号括上
  
   if x != 5 {
            … …
   }

十、查看文档

Go的全量文档几乎与Go安装包一起发布。安装Go后,执行godoc –http=:端口号即可启动doc server。打开浏览器,输入http://localhost:端口号即可以看到几乎与Go官方站完全相同的文档页面。

十一、参考书籍

Go毕竟是新生代语言,其自身尚不成熟和完善,资料也较少。这里推荐两本市面上比较好的且内容已更新到Go 1的书籍:

* Mark Summerfield的《Programming in Go: creating applications for the 21st century
* Ivo Balbaert的《The Way to Go – A Thorough Introduction to the Go Programming Language

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats