Skip to content

1. 安装 Go

shell
$ brew install go

$ go version

从 Go 1.11 版本开始,Go 使用 Go Modules 管理模块,推荐设置以下环境变量,通过国内镜像下载第三方包

shell
$ go env -w GOPROXY=https://goproxy.cn,direct

2. Hello World

go
// 声明包,一般一个文件夹对应一个包,以包为单位暴露类型和方法供包外部调用
package main
// 导包,fmt 是标准库,用于处理标准输入输出
import "fmt"
// 方法,main 方法是这个程序入口,且必须位于 main 包下
func main() {
    // 打印 
    fmt.Print("Hello World!")
}

运行

shell
# 解释执行(实际是编译成a.out再执行)
go run main.go
# 或者
go run .
# 编译执行
go build main.go && ./main

格式化:

go
package main

import "fmt"
import "math"

func main() {
    fmt.Println("hello world")

    fmt.Printf("%t\n", 1==2)
    fmt.Printf("二进制:%b\n", 255)
    fmt.Printf("八进制:%o\n", 255)
    fmt.Printf("十六进制:%X\n", 255)
    fmt.Printf("十进制:%d\n", 255)
    fmt.Printf("浮点数:%f\n", math.Pi)
    fmt.Printf("字符串:%s\n", "hello world")
}

3. 变量与类型

变量

go
// 格式:声明变量 变量名 变量类型
var a int = 1
// 声明初始化多个变量
var  i, j, k int = 1, 2, 3
// 默认值为该类型的零值
var a int
// 根据值推断变量类型
var a = 1
// 使用 := 符合省略 var
a := 1
msg := "Hello World!"

简单类型

  • 空值 nil
  • 整数类型 int(取决于操作系统), int8, int16, int32, int64, uint8, uint16。。。
  • 浮点数类型:float32, float64
  • 字节类型:byte (等价于uint8)
  • 字符串类型:string
  • 布尔值类型:boolean,(true 或 false)
go
var a int8 = 10
var c1 byte = 'a'
var b float32 = 12.2
var msg = "Hello World"
ok := false

字符串

Go 语言字符串使用 UTF-8 编码,英文占 1 个字节,中文占 3 个字节,字符串用 byte 数组保存,类型是 uint8,打印数组中的值需要使用 string 进行类型转换,否则打印的是编码值。字符串的长度是数组的长度而不是字符个数,使用以下代码验证:

go
package main

import (
	"fmt"
	"reflect"
)
func main() {
    str1 := "Golang"
    str2 := "Go语言"
    // uint8
    fmt.Println(reflect.TypeOf(str2[2]).Kind()) 
    // 108 l
    fmt.Println(str1[2], string(str1[2]))       
    // 232 è
    fmt.Printf("%d %c\n", str2[2], str2[2])     
    // len(str2): 8
    fmt.Println("len(str2):", len(str2))       
}

转换成 []rune 类型后,字符串中的每个字符,无论占多少个字节都用 int32 来表示,因而可以正确处理中文

数组与切片

数组:

go
// 声明数组
var arr [5]int     
// 二维数组
var arr2 [5][5]int 
// 带初始化值的声明
var arr = [5]int{1, 2, 3, 4, 5}
arr := [5]int{1, 2, 3, 4, 5}
go
arr := [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
	arr[i] += 100
}
// [101 102 103 104 105]
fmt.Println(arr)

数组的长度初始化后就不能改变

切片是数组的抽象,使用数组作为底层结构,切片可以随时进行扩展,切片包含三个组件: 容量,长度和指向底层数组的指针,

声明切片:

go
// 长度为0的切片
slice1 := make([]float32, 0) 
// [0 0 0] 长度为3容量为5的切片
slice2 := make([]float32, 3, 5) 
// 3 5
fmt.Println(len(slice2), cap(slice2))

切片的基本操作

go
// 添加元素,切片容量可以根据需要自动扩展,这里 append 后产生了新的切片对象,所以需要将 slice2 对象指向新的引用,否则 slice2 对象是没有发生变化的
// [0, 0, 0, 1, 2, 3, 4]
slice2 = append(slice2, 1, 2, 3, 4) 
// 7 12
fmt.Println(len(slice2), cap(slice2)) 
// 子切片 [start, end)
sub1 := slice2[3:] // [1 2 3 4]
sub2 := slice2[:3] // [0 0 0]
sub3 := slice2[1:4] // [0 0 1]
// 合并切片
// [1, 2, 3, 4, 0, 0, 0]
combined := append(sub1, sub2...)
  • 声明切片时可以为切片设置容量大小,为切片预分配空间。在实际使用的过程中,如果容量不够,切片容量会自动扩展。
  • sub2... 是切片解构的写法,将切片解构为 N 个独立的元素

map

go
// 创建空 map
m1 := make(map[string]int)
// 初始化赋值
m2 := map[string]string{
	"Sam": "Male",
	"Alice": "Female",
}
// 赋值/修改
m1["Tom"] = 18
// 读 map 里的值
v := m1["two"] 
fmt.Println(v)
// 删除键值
delete(m1, "two")
m3 := map[string]int{"one": 1, "two": 2, "three": 3}
// 输出 map[two:2 three:3 one:1] (顺序在运行时可能不一样)
fmt.Println(m3) 
for key, val := range m3 {
    fmt.Printf("%s => %d \n", key, val)
}
// 输出:(顺序在运行时可能不一样)
// three => 3
// one => 1
// two => 2

指针

指针即某个值的地址,类型定义时使用符号 *,对一个已经存在的变量,使用 & 获取该变量的地址。

go
str := "Golang"
// p 是指向 str 的指针
var p *string = &str 
*p = "Hello"
// Hello 修改了 p,str 的值也发生了改变
fmt.Println(str)

指针通常用于函数传递参数,或者给类型定义新的方法。Go 语言中,参数是按值传递的,如果不使用指针,函数内部将会拷贝一份参数的副本,对参数的修改并不会影响到外部变量的值。如果参数使用指针,对参数的传递将会影响到外部变量。

go
func add(num int) {
    num += 1
}

func realAdd(num *int) {
    *num += 1
}

func main() {
    num := 100
    // 100,num 没有变化
    add(num)
    fmt.Println(num)  

    // 101,指针传递,num 被修改
    realAdd(&num)
    fmt.Println(num)  
}

内存分配

内建分配内存函数 new 和 make: new 是一个分配内存的内建函数,不同于其他语言中 new 所所的工作,它只是将内存清零,而不是初始化内存。new(T) 为一个类型为 T 的新项目分配了值为零的存储空间并返回其地址,也就是一个类型为 *T 的值。用Go的术语来说,就是它返回了一个指向新分配的类型为T的零值的指针。

make(T, args) 函数的目的与 new(T) 不同。它仅用于创建切片、map 和 channel(信道),并返回类型 T(不是 *T)的一个被初始化了的(不是零)实例。这种差别的出现是由于这三种类型实质上是对在使用前必须进行初始化的数据结构的引用。例如,切片是一个具有三项内容的描述符,包括指向数据(在一个数组内部)的指针、长度以及容量,在这三项内容被初始化之前,切片值为 nil。对于切片、映射和信道,make初始化了其内部的数据结构并准备了将要使用的值。如:

下面的代码分配了一个整型数组,长度为10,容量为100,并返回前10个数组的切片

go
make([]int, 10, 100)

以下示例说明了new和make的不同。

go
// 为切片结构分配内存;*p == nil;很少使用
var p *[]int = new([]int)   
// 切片v现在是对一个新的有10个整数的数组的引用
var v  []int = make([]int, 10) 

// 不必要地使问题复杂化:
var p *[]int = new([]int)
fmt.Println(p) //输出:&[]
*p = make([]int, 10, 10)
fmt.Println(p) //输出:&[0 0 0 0 0 0 0 0 0 0]
fmt.Println((*p)[2]) //输出: 0

// 习惯用法:
v := make([]int, 10)
fmt.Println(v) //输出:[0 0 0 0 0 0 0 0 0 0]

4. 流程控制(if, for, switch)

条件语句

go
age := 18
if age < 18 {
    fmt.Printf("Kid")
} else {
    fmt.Printf("Adult")
}

// 可以简写为:
if age := 18; age < 18 {
    fmt.Printf("Kid")
} else {
    fmt.Printf("Adult")
}

switch

go
type Gender int8
// 常量
const (
    MALE   Gender = 1
    FEMALE Gender = 2
)

gender := MALE

switch gender {
case FEMALE:
    fmt.Println("female")
case MALE:
    fmt.Println("male")
default:
    fmt.Println("unknown")
}

在这里,使用了type 关键字定义了一个新的类型 Gender,是 int8 的别名。 使用 const 定义了 MALE 和 FEMALE 2 个常量,Go 语言中没有枚举(enum)的概念,可以用常量的方式来模拟枚举。 Go 语言的 switch 不需要 break,匹配到某个 case,执行完该 case 定义的行为后,默认不会继续往下执行。如果需要继续往下执行,需要使用 fallthrough

go
switch i {
case 1, 2, 3:
    fmt.Println("one, two, three")
    fallthrough
case 4:
    fmt.Println("four")
    fallthrough
default:
    fmt.Println("unknown")
}
// 输出结果
// male
// unknown

for 循环

一个简单的累加的例子,break 和 continue 的用法与其他语言没有区别。

go
sum := 0
// 经典的for语句 init; condition; post
for i := 0; i < 10; i++ {
	if sum > 50 {
		break
	}
	sum += i
}

// 精简的for语句 condition
i := 1
for i<10 {
    fmt.Println(i)
    i++
}

//死循环的for语句 相当于for(;;)
i :=1
for {
    if i>10 {
        break
    }
    i++
}

对数组(arr)、切片(slice)、字典(map) 使用 for range 遍历:

go
nums := []int{10, 20, 30, 40}
for i, num := range nums {
	fmt.Println(i, num)
}
// 0 10
// 1 20
// 2 30
// 3 40
m2 := map[string]string{
	"Sam":   "Male",
	"Alice": "Female",
}

for key, value := range m2 {
	fmt.Println(key, value)
}
// Sam Male
// Alice Female

5. 函数(functions)

参数与返回值

一个典型的函数定义如下,使用关键字 func,参数可以有多个,返回值也支持有多个

go
func funcName(param1 Type1, param2 Type2, ...) (return1 Type3, ...) {
    // body
}
go
func add(num1 int, num2 int) int {
	return num1 + num2
}

func div(num1 int, num2 int) (int, int) {
	return num1 / num2, num1 % num2
}
func main() {
    quo, rem := div(100, 17)
    // 5 15
    fmt.Println(quo, rem)     
    // 117
    fmt.Println(add(100, 17)) 
}

可以给返回值命名,简化 return

go
func add(num1 int, num2 int) (ans int) {
	ans = num1 + num2
	return
}

可变参数

go
func sum(nums ...int) {
    //输出如 [1, 2, 3] 之类的数组
    fmt.Print(nums, " ")  
    total := 0
    //要的是值而不是下标
    for _, num := range nums { 
        total += num
    }
    fmt.Println(total)
}
func main() {
    sum(1, 2)
    sum(1, 2, 3)

    //传数组
    nums := []int{1, 2, 3, 4}
    sum(nums...)
}

错误处理(error handling)

通过返回值判断方法是否执行异常

go
import (
	"fmt"
	"os"
)

func main() {
	_, err := os.Open("filename.txt")
	if err != nil {
		fmt.Println(err)
	}
}
// open filename.txt: no such file or directory

可以通过 errors.New 返回自定义的错误

go
import (
	"errors"
	"fmt"
)

func hello(name string) error {
	if len(name) == 0 {
		return errors.New("error: name is null")
	}
	fmt.Println("Hello,", name)
	return nil
}

func main() {
	if err := hello(""); err != nil {
		fmt.Println(err)
	}
}
// error: name is null

error 往往是能预知的错误,但是也可能出现一些不可预知的错误,例如数组越界,这种错误可能会导致程序非正常退出,在 Go 语言中称之为 panic。

go
func get(index int) int {
	arr := [3]int{2, 3, 4}
	return arr[index]
}

func main() {
	fmt.Println(get(5))
	fmt.Println("finished")
}
shell
go run .
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
exit status 2

异常处理机制:defer recover

go
func get(index int) (ret int) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Some error happened!", r)
			ret = -1
		}
	}()
	arr := [3]int{2, 3, 4}
	return arr[index]
}

func main() {
	fmt.Println(get(5))
	fmt.Println("finished")
}
shell
go run .
Some error happened! runtime error: index out of range [5] with length 3
-1
finished

在 get 函数中,使用 defer 定义了异常处理的函数,在方法执行出现异常时,执行 defer 定义的代码。因此如果触发了 panic,控制权就交给了 defer。 在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且做一些简单的处理,通常用于资源释放

panic 通常库函数不应该使用 panic

go
func g(i int) {
    if i>1 {
        fmt.Println("Panic!")
        panic(fmt.Sprintf("%v", i))
    }
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()

    for i := 0; i < 4; i++ {
        fmt.Println("Calling g with ", i)
        g(i)
        fmt.Println("Returned normally from g.")
     }
}

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

与 Java 相比,panic 类似抛 Exception,recover 类似 catch,defer 类似 finally

闭包

nextNum 这个函数返回了一个匿名函数,这个匿名函数记住了 nextNum中i+j 的值,并改变了 i,j 的值,于是形成了一个闭包的用法

go
func nextNum() func() int {
    i,j := 1,1
    return func() int {
        var tmp = i+j
        i, j = j, tmp
        return tmp
    }
}
// main 函数中是对 nextNum 的调用,其主要是打出下一个斐波拉契数
func main(){
    nextNumFunc := nextNum()
    for i:=0; i<10; i++ {
        fmt.Println(nextNumFunc())
    }
}

结构体,方法和接口

结构体(struct) 和方法(methods)

结构体类似于其他语言中的 class,可以在结构体中定义多个字段,为结构体实现方法,实例化等。

go
type Student struct {
	name string
	age  int
}

func (stu *Student) hello(person string) string {
	return fmt.Sprintf("hello %s, I am %s", person, stu.name)
}

func main() {
	stu := &Student{
		name: "Tom",
	}
	msg := stu.hello("Jack")
	fmt.Println(msg) // hello Jack, I am Tom
}

使用 Student{field: value, ...}的形式创建 Student 的实例,字段的值默认零值。 在 func 和函数名hello 之间,加上该方法对应的实例名 stu 及其类型 *Student,可以通过实例名访问该实例的字段name和其他方法了。 调用方法通过 实例名.方法名(参数) 的方式。

go
func main() {
    // 使用 new 实例化
    stu2 := new(Student)
    // hello Alice, I am  , name 被赋予默认值""
	fmt.Println(stu2.hello("Alice")) 
}

接口(interfaces)

接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口

go
type Person interface {
	getName() string
}

type Student struct {
	name string
	age  int
}

func (stu *Student) getName() string {
	return stu.name
}

type Worker struct {
	name   string
	gender string
}

func (w *Worker) getName() string {
	return w.name
}

func main() {
	var p Person = &Student{
		name: "Tom",
		age:  18,
	}
    // Tom
	fmt.Println(p.getName()) 
}

Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可。 实例化 Student后,强制类型转换为接口类型 Person。强转时如果没有实现接口中的所有方法,则会在编译时报错

接口转为实现类:

go
func main() {
	var p Person = &Student{
		name: "Tom",
		age:  18,
	}
    // 接口转为实现类
	stu := p.(*Student) 
	fmt.Println(stu.getAge())
}

多态示例

go
//---------- 接 口 --------//
type shape interface {
    // 计算面积
    area() float64 
    // 计算周长
    perimeter() float64 
}

//--------- 长方形 ----------//
type rect struct {
    width, height float64
}
// 面积
func (r *rect) area() float64 { 
    return r.width * r.height
}
// 周长
func (r *rect) perimeter() float64 { 
    return 2*(r.width + r.height)
}

//----------- 圆  形 ----------//
type circle struct {
    radius float64
}
// 面积
func (c *circle) area() float64 { 
    return math.Pi * c.radius * c.radius
}
// 周长
func (c *circle) perimeter() float64 { 
    return 2 * math.Pi * c.radius
}

// ----------- 接口的使用 -----------//
func interface_test() {
    r := rect {width:2.9, height:4.8}
    c := circle {radius:4.3}

    // 通过指针实现
    s := []shape{&r, &c} 

    for _, sh := range s {
        fmt.Println(sh)
        fmt.Println(sh.area())
        fmt.Println(sh.perimeter())
    }
}

空接口

如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型

go
func main() {
	m := make(map[string]interface{})
	m["name"] = "Tom"
	m["age"] = 18
    m["scores"] = [3]int{98, 99, 85}
    // map[age:18 name:Tom scores:[98 99 85]]
	fmt.Println(m) 
}

并发编程(goroutine)

sync

Go 语言提供了 sync 和 channel 两种方式支持协程(goroutine)的并发。

例如我们希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用 sync.WaitGroup,等待所有并发协程执行结束。

go
import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func download(url string) {
    fmt.Println("start to download", url)
    // 模拟耗时操作
    time.Sleep(time.Second) 
    wg.Done()
}

func main() {
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go download("a.com/" + string(i+'0'))
	}
	wg.Wait()
	fmt.Println("Done!")
}

wg.Add(1):为 wg 添加一个计数,wg.Done(),减去一个计数。

go download():启动新的协程并发执行 download 函数。

wg.Wait():等待所有的协程执行结束

channel

go
// 创建大小为 10 的缓冲信道
var ch = make(chan string, 10) 

func download(url string) {
    fmt.Println("start to download", url)
    time.Sleep(time.Second)
    // 将 url 发送给信道
    ch <- url 
}

func main() {
	for i := 0; i < 3; i++ {
		go download("a.com/" + string(i+'0'))
	}
	for i := 0; i < 3; i++ {
    // 等待信道返回消息。
		msg := <-ch 
		fmt.Println("finish", msg)
	}
	fmt.Println("Done!")
}

使用 channel 信道,可以在协程之间传递消息。阻塞等待并发协程返回消息。

shell
$ go run .
start to download a.com/2
start to download a.com/0
start to download a.com/1
finish a.com/2
finish a.com/1
finish a.com/0
Done!

real    0m1.528s

channel 默认上是阻塞的,也就是说,如果Channel满了,就阻塞写,如果Channel空了,就阻塞读。于是,我们就可以使用这种特性来同步我们的发送和接收端。 示例

go
func main() {
    channel := make(chan string) //注意: buffer为1

    go func() {
        channel <- "hello"
        fmt.Println("write \"hello\" done!")

        channel <- "World" //Reader在Sleep,这里在阻塞
        fmt.Println("write \"World\" done!")

        fmt.Println("Write go sleep...")
        time.Sleep(3*time.Second)
        channel <- "channel"
        fmt.Println("write \"channel\" done!")
    }()

    time.Sleep(2*time.Second)
    fmt.Println("Reader Wake up...")

    msg := <-channel
    fmt.Println("Reader: ", msg)

    msg = <-channel
    fmt.Println("Reader: ", msg)

    msg = <-channel //Writer在Sleep,这里在阻塞
    fmt.Println("Reader: ", msg)
}

多个 channel 的 select

go
func main() {
    //创建两个channel - c1 c2
    c1 := make(chan string)
    c2 := make(chan string)

    //创建两个goruntine来分别向这两个channel发送数据
    go func() {
        time.Sleep(time.Second * 1)
        c1 <- "Hello"
    }()
    go func() {
        time.Sleep(time.Second * 1)
        c2 <- "World"
    }()

    //使用select来侦听两个channel
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}

Channel select 阻塞的 Timeout

go
for {
        timeout_cnt := 0
        select {
        case msg1 := <-c1:
            fmt.Println("msg1 received", msg1)
        case msg2 := <-c2:
            fmt.Println("msg2 received", msg2)
        case  <-time.After(time.Second * 30):
            fmt.Println("Time Out")
            timout_cnt++
        }
        if time_cnt > 3 {
            break
        }
    }

Channel 的无阻塞

go
for {
      select {
      case msg1 := <-c1:
          fmt.Println("received", msg1)
      case msg2 := <-c2:
          fmt.Println("received", msg2)
      default: //default会导致无阻塞
          fmt.Println("nothing received!")
          time.Sleep(time.Second)
      }
    }

Channel 的关闭

go
func main() {

    channel := make(chan string)
    rand.Seed(time.Now().Unix())

    //向channel发送随机个数的message
    go func () {
        cnt := rand.Intn(10)
        fmt.Println("message cnt :", cnt)
        for i:=0; i<cnt; i++{
            channel <- fmt.Sprintf("message-%2d", i)
        }
        close(channel) //关闭Channel
    }()

    var more = true
    var msg string
    for more {
        select{
        // channel会返回两个值,一个是内容,一个是还有没有内容
        case msg, more = <- channel:
            if more {
                fmt.Println(msg)
            }else{
                fmt.Println("channel closed!")
            }
        }
    }
}

单元测试(unit test)

假设我们希望测试 package main 下 calc.go 中的函数,要只需要新建 calc_test.go 文件,在calc_test.go中新建测试用例即可。

go
// calc.go
package main

func add(num1 int, num2 int) int {
	return num1 + num2
}
go
// calc_test.go
package main

import "testing"

func TestAdd(t *testing.T) {
	if ans := add(1, 2); ans != 3 {
		t.Error("add(1, 2) should be equal to 3")
	}
}

运行 go test,将自动运行当前 package 下的所有测试用例,如果需要查看详细的信息,可以添加-v参数

shell
$ go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      example 0.040s

包(Package)和模块(Modules)

Package

一般来说,一个文件夹可以作为 package,同一个 package 内部变量、类型、方法等定义可以相互看到。

比如我们新建一个文件 calc.go, main.go 平级,分别定义 add 和 main 方法

go
// calc.go
package main

func add(num1 int, num2 int) int {
	return num1 + num2
}
go
// main.go
package main

import "fmt"

func main() {
  // 8
	fmt.Println(add(3, 5)) 
}

运行 go run main.go,会报错,add 未定义:

shell
$ ./main.go:6:14: undefined: add

因为 go run main.go 仅编译 main.go 一个文件,所以命令需要换成

shell
$ go run main.go calc.go
8

shell
$ go run .
8

Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型/接口/方法/函数/字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见。

Modules

在一个空文件夹下,初始化一个 Module

shell
$ go mod init example
go: creating new go.mod: module example

此时,在当前文件夹下生成了go.mod,这个文件记录当前模块的模块名以及所有依赖包的版本。

接着,我们在当前目录下新建文件 main.go,添加如下代码:

go
package main

import (
	"fmt"

	"rsc.io/quote"
)

func main() {
  // Ahoy, world!
	fmt.Println(quote.Hello())  
}

运行 go run .,将会自动触发第三方包 rsc.io/quote 的下载,具体的版本信息也记录在了go.mod中:

module example

go 1.13

require rsc.io/quote v3.1.0+incompatible

我们在当前目录,添加一个子 package calc,代码目录如下:

demo/
   |--calc/
      |--calc.go
   |--main.go

在 calc.go 中写入

go
package calc

func Add(num1 int, num2 int) int {
	return num1 + num2
}

在 package main 中如何使用 package cal 中的 Add 函数呢?import 模块名/子目录名 即可,修改后的 main 函数如下:

go
package main

import (
	"fmt"
	"example/calc"
	"rsc.io/quote"
)

func main() {
	fmt.Println(quote.Hello())
	fmt.Println(calc.Add(10, 3))
}
shell
$ go run .
Ahoy, world!
13

参考

Go简明教程

Go 语言简介(上)- 语法

Go 语言简介(下)- 特性