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<     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

© 2012, bigwhite. 版权所有.

本文来自:Tony Bai

感谢作者:bigwhite

查看原文:Go程序设计语言(一)

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。