给 Java 程序员写的 Go 语言入门
Nov 08, 2016
Go,又称 golang,是 Google 开发的一种静态强类型、编译型,并发型,并具有垃圾回收功能的编程语言。
罗伯特·格瑞史莫、罗勃·派克及肯·汤普逊于 2007 年 9 月开始设计 Go 语言,于 2009 年 11 月正式宣布推出,成为开放源代码项目。
1. 环境
1.1. GOROOT和GOPATH
- GOROOT:Go 的安装路径;
- GOPATH:Go 的工作路径,类比 Java 中的 CLASSPATH。GOPATH 可以有多个,Windows-based OS 以分号分隔;Unix-based OS 以冒号分隔。
注: GOROOT 不能和 GOPATH 相同。
设置 PATH = $GOROOT/bin:$GOPATH/bin:$PATH
方便直接运行程序。
1.2. 目录结构
GOPATH 下包含三个目录:bin
、pkg
和 src
。
bin
:存放编译后的可执行文件;pkg
:存放引用的包(package),类比 Java CLASSPATH 引用的第三方库;src
:存放源代码。
一般的工程目录为 $GOPATH/src/github.com/<github_username>/<project_name>
。
1.3. Go 命令行
go build // 编译源代码,编译后的目标文件放在 pkg 目录下
go run // 编译源代码,并从入口函数执行程序
go install // 从入口函数程序编译代码,编译后的可执行程序放在 bin 目录下
1.4. 测试程序
源文件后加 _test
的文件是此代码的单体测试代码。例如,string.go
的单体测试程序是 string_test.go
。
使用 go test
运行测试程序。
1.5. 开发IDE
2. 语法
2.1. 语言结构
2.1.1. 包
Go 语言由包(package)组成。Go 语言中的包类比 Java 中的 类(class)。
首字母大写的名称被导出包。 类比 Java 中的 public 方法和 public 类的成员变量。
程序运行的入口是 package main
。
// 写法一
import "fmt"
import "math"
// 写法二
import (
"fmt"
"math"
)
2.1.2. 语句
Go 语言语句后可以用分号结尾,也可以不用分号结尾。
2.1.3. 注释
C 风格,和 Java 相同。
2.1.4. 运算符
C 风格,和 Java 相同。
2.2. 变量
2.2.1. 数据类型
基本数据类型有布尔(bool)、数字(int, float, …)和字符串(string)三种。
数字类型又包含 uint, uint8(byte), uint16, uint32, uint64, int, int8, int16, int32(rune), int64, float, float32, float64, complex64, complex128, uintptr等。
衍生类型又包括 指针(pointer)、数组(array)、结构体(struct)、联合体(union)、函数(func)、切片(slice)、接口(interface)、管道(channel)等。
Go 语言需要显式转换变量类型。
aaa := 42
bbb := float64(aaa)
ccc := uint(bbb)
2.2.2. 声明变量
使用 var
声明变量,类型写在变量名后面。
当声明多个变量连续为同一类型时,除最后一个类型外,其他可以省略。
var aaa, bbb, ccc bool
// 另一种写法
var (
aaa bool
bbb bool
ccc bool
)
变量可以定义在包级别(函数外部)和函数级别。类比 Java 中类的成员变量和方法内部的局部变量。
2.2.3. 初始化变量
声明变量时,可以初始化变量。每个变量依次对应一个值。
初始化变量不指定类型时,变量从初始值中获得类型。
var i, j int = 1, 2
var aaa, bbb, ccc = true, 0, "no!"
在函数内部,使用 :=
初始化变量时,可以省略 var
。在函数外部不可省略。
aaa, bbb, ccc := true, 0, "no!"
变量没有初始化,系统自动赋零值。数值的零值是 0
,布尔是 false
,字符串是 ""
(空字符串)。
2.2.4. 常量
常量使用关键字 const
声明。
常量只能是字符串、布尔和数字三种类型。
常量不能使用 :=
符号赋值声明。
const aaa, bbb, ccc = 1, false, "str"
常量的枚举写法,
const (
UNKNOWN = 0
FEMALE = 1
MALE = 2
)
常量赋值时,如果使用函数,函数必须为内置函数。
const (
aaa = "abc"
bbb = len(aaa)
ccc = unsafe.Sizeof(aaa)
)
iota
是一个可以被编译器修改的常量。在每一个 const
关键字出现时,被重置为 0
,然后再下一个 const
出现之前,每出现一次 iota
,其所代表的数字会自动增加 1
。
// 例一
const (
aaa = iota // aaa = 0
bbb = iota // bbb = 1
ccc = iota // ccc = 2
)
// 例二
const (
aaa = iota // aaa = 0
bbb // bbb = 1
ccc // ccc = 2
ddd = "ha" // ddd = "ha", iota += 1
eee // eee = "ha", iota += 1
fff = 100 // fff = 100, iota +=1
ggg // ggg = 100, iota +=1
hhh = iota // hhh = 7
iii // iii = 8
)
2.3. 流程控制语句
2.3.1. 循环
2.3.1.1. for 循环
for init; condition; post { ... }
// 例
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
- init 初始化语句,在第一次循环执行前被执行;
- condition 循环条件表达式,每轮迭代开始前被求值;
- post 后置语句,每轮迭代后被执行。
循环初始化语句和后置语句都是可选的。
2.3.1.2. while 循环
for condition { ... }
// 例
sum := 1
for sum < 1000 {
sum += sum
}
2.3.1.3. 集合迭代
for key, value := range old_collection {
new_collection[key] = value
}
Go 支持 break
、continue
和 goto
语句。三种语句的用法和 Java 相同。
2.3.2. 分支
2.3.2.1. if 语句
if init; condition { ... }
// 例
if aaa := math.Pow(bbb, n); aaa < ccc {
// ...
}
// init 可省略
aaa := math.Pow(bbb, n)
if aaa < ccc {
// ...
}
Go 语言支持 if-else
,用法和 Java 相同。
2.3.2.2. switch 语句
switch
的条件从上到下的执行,当匹配成功的时候停止,不需要 break
。
没有条件的 switch
同 switch true
一样。这一构造使得可以用更清晰的形式来编写长的 if-then-else
链。
var grade string = "B"
var marks int = 90
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("及格\n" )
case grade == "D":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" );
}
2.3.3. 延迟
defer
语句会延迟函数的执行直到上层函数返回。
延迟函数的参数会立刻被执行,但是在上层函数返回前函数都不会被调用。
延迟的函数调用被压入一个栈中。当函数返回时, 会按照后进先出的顺序调用被延迟的函数调用。
// 以下代码输出 hello world
defer fmt.Println("world")
fmt.Println("hello")
2.4. 函数
2.4.1. 参数
函数参数的类型在变量名之后。函数参数的类型可以省略,除了最后一个参数。
func add(x, y int) int {
return x + y
}
Go 语言使用值传递,函数对形参的修改不会影响到实参。引用传递需要依赖传递参数的指针实现。
// 引用传递
func swap(x *int, y *int) {
var temp int
temp = *x /* 保持 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
2.4.2. 返回值
函数可以有任意返回值,返回值类型在函数之后。
func swap(x, y string) (string, string) {
return y, x
}
Go 的返回值可以被命名,被命名的返回值相当于在函数体开头声明的变量。
没有参数的 return 语句返回各个返回变量的当前值。
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
2.4.3. 函数句柄
Go 语言中函数也是值。他们可以像其他值一样传递,比如,函数值可以作为函数的参数或者返回值。
// 声明 myfunc 为一匿名函数的句柄
myfunc := func(x int) int {
return x * x
}
// 定义一个接收函数句柄为参数的函数
func yourfunc (fn func(int) int) int {
return fn(3)
}
// 打印 9
fmt.Println(yourfunc(myfunc))
2.4.4. 闭包
Go 函数可以是一个闭包。闭包是一个函数值,它引用了函数体之外的变量。 这个函数可以对这个引用的变量进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。
// 声明一个闭包函数,返回值是另一个匿名函数体
func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}
// 设置匿名函数句柄
nextNumber := getSequence()
// 打印闭包中 i 的值,输出为1,2,3
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())
3. 数据结构
3.1. 指针和结构体
3.1.1. 指针
类型 *T 是指向类型 T 的值的指针,其零值是 nil
。nil
指针也称为空指针。
&
符号(取地址符)会生成一个指向其作用对象的指针。*
符号(取值符)表示指针指向的对象的值。
一个指针变量通常缩写为 ptr
。
Go 支持指向指针的指针。
与 C 不同,Go 没有指针运算。
var a int = 20 /* 声明实际变量 */
var ptr *int /* 声明指针变量 */
ptr = &a /* 指针变量 ptr 的值为变量 a 的地址 */
var pptr **int /* 指向 ptr 指针的指针 */
pptr = &ptr /* 指向 ptr 的地址 */
3.1.2. 结构体
结构体使用 type
关键字声明。结构体使用大括号赋值,使用点访问成员变量。结构体字段可以通过结构体指针透明访问。
type Vertex struct {
X int
Y int
}
var v Vertex
v = Vertex{1, 2}
v.X = 4
p := &v
p.Y = 5
3.2. 集合
3.2.1. 数组
数组的长度不可改变。
// 声明数组
var balance [10] float32
// 初始化
var balance = [5] float32 {1000.0, 2.0, 3.4, 7.0, 50.0}
// 访问数组
float32 salary = balance[2]
// 声明三维数组
var threedim [5][10][4] int
// 初始化二维数组
aaa = [3][4] int {
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11} /* 第三行索引为 2 */
}
// 访问二维数组
int val = a[2][3]
3.2.2. 切片
切片是对数组的抽象。切片的长度可变。
// 声明切片
var aaa [] int
// 使用 make 函数创建切片,长度为 10
var aaa []int = make([]int, 10)
// 初始化切片,初始化值依次是1, 2, 3。其cap=len=3
aaa :=[] int {1, 2, 3}
// 从数组 aaa 创建切片 bbb
bbb := aaa[:]
// 从切片 bbb 的第 2 个元素开始到第 5 个元素结束,创建切片 ccc
// 截取规则: 前包含,后不包含
ccc := bbb[1:6]
len
函数可以获取切片的长度,cap
函数可以获取切片最长可达多长。
copy
函数用来拷贝切片,可以增加切片的容量;append
函数向切片中追加元素。
切片的零值是 nil
,一个 nil
的切片的长度和容量是 0
。
3.2.3. range
当使用 for 循环遍历一个切片时,每次迭代 Range 将返回两个值。 第一个是当前下标(序号),第二个是该下标所对应元素的一个拷贝。
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
可以通过赋值给 _
来忽略序号和值;如果只需要索引值,去掉 , value
的部分即可。
pow := make([]int, 10)
for i := range pow {
pow[i] = 1 << uint(i)
}
for _, value := range pow {
fmt.Printf("%d\n", value)
}
Range 用在 map 上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
Range 用在字符串上。
for i, c := range "go" {
fmt.Println(i, c)
}
3.2.4. map
Map 在使用之前必须用 make
来创建;值为 nil
的 map 是空的,并且不能对其赋值。
Map 是无序的。
type Vertex struct {
Lat, Long float64
}
// 初始化 map 方式一
var aaa map[string]Vertex
aaa = make(map[string]Vertex)
aaa["Bell Labs"] = Vertex{40.68433, -74.39967,}
aaa["Google"] = Vertex{37.42202, -122.08408,}
// 初始化 map 方式二
var aaa = map[string]Vertex{
"Bell Labs": Vertex{40.68433, -74.39967,},
"Google": Vertex{37.42202, -122.08408,},
}
// 初始化 map 方式三
var aaa = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
// 在 map 中插入或修改元素
aaa["Bell Labs"] = Vertex{37.42202, -122.08408,}
// 获取 map 中的元素
var v = aaa["Bell Labs"]
// 删除 map 中的元素
delete(aaa, "Bell Labs")
// 检测 map 中是否存在某个元素。如果存在 ok 返回 true;否则 ok 返回 false,v 是零值
v, ok := aaa["Bell Labs"]
3.3. 面向对象
3.3.1. 方法
Go 没有类,Go 中的方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。
// 命名类型
type MathInt int
func (f MathInt) Abs() int {
if f < 0 {
return int(-f)
}
return int(f)
}
// 结构体类型
type Vertex struct {
X, Y int
}
// 有两个原因需要使用接受者指针:首先避免在每个方法调用时拷贝值;其次,方法可以修改接收者指向的值。
func (v *Vertex) Abs() int {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
3.3.2. 接口
接口是一组方法定义的集合。它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
/* 定义接口 */
type Phone interface {
call()
}
/* 定义结构体 */
type NokiaPhone struct {
// ...
}
type IPhone struct {
// ...
}
/* 实现接口方法 */
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
/* 调用接口 */
var phone Phone
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
隐式接口解耦了实现接口和定义接口,无需显式 implements
声明。例如,在 Java 中重载 toString 方法,在 Go 中如下,
// toString 在 Go 中的原型是
/*
type Stringer interface {
String() string
}
*/
type Person struct {
Name string
Age int
}
// 实现 Person 的 Stringer 接口,重载 String 方法
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z)
}
4. 错误
4.1. 错误处理
Go 不支持 try…catch…finally
处理异常,而使用 error 值来表示错误状态。
通常函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 nil
, 来进行错误处理。error 为 nil
时表示成功;非 nil
的 error 表示错误。函数通常在最后的返回值中返回错误信息。
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
自定义 error 应实现内建的 error 接口。
type error interface {
Error() string
}
4.2. panic 和 recover
除了多值返回处理错误以外,Go 中可以抛出一个 panic
的异常,然后在 defer
中通过 recover
捕获这个异常,然后正常处理。
// 主函数中,defer 一个匿名函数用来错误处理
defer func() {
fmt.Println("第二步")
if err := recover(); err != nil {
fmt.Println(err) // 通过 recover 获取的 err 就是 panic 传入的值:"I'm Error!"
}
fmt.Println("第三步")
}()
myfunc()
// 以下是 myfunc
func myfunc() {
fmt.Println("第一步")
panic("I'm Error!")
fmt.Println("走不到的第四步")
}
5. 并发
5.1. 协程
协程(goroutine),是由 Go 运行时环境管理的轻量级线程。
使用 go
关键字 go myfunc(x, y, z)
开启一个新的协程执行 myfunc(x, y, z)
。myfunc
和其参数是当前协程中定义的,但是在新的协程中运行。
Go 的协程在相同的地址空间中运行,因此访问共享内存必须进行同步。
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
5.2. 管道
协程之间使用管道(channel)通讯。管道是有类型的。使用管道操作符 <-
发送或接收值。箭头表示数据流的方向。
ch <- v // 把变量 v 发送到管道 ch
v := <- ch // 从管道 ch 中接收,并赋值给 v
管道使用前必须创建。
ch := make(chan int) // 创建 int 型管道
默认情况下,在另一端准备好之前,发送和接收都会阻塞。这使得协程可以在没有明确的锁或竞态变量的情况下进行同步。
管道可以带缓冲(buffer)。make 函数提供第二个参数作为缓冲长度来初始化一个缓冲管道。
ch := make(chan int, 100) // 创建缓冲区为 100 个整型的 int 型管道
向带缓冲的管道发送数据的时候,只有在缓冲区满的时候才会阻塞。 而当缓冲区为空的时候接收操作会阻塞。
发送者可以关闭一个管道来表示再没有值会被发送了。接收者可以通过赋值语句的第二参数来测试管道是否被关闭。
只有发送者才能关闭管道,但是通常情况下管道无需关闭。
v, ok := <- ch // 管道关闭,ok 返回 false
for v := rang ch {...} // 通过 range 循环,可以不断从管道中取值,直到管道关闭
5.3. select
select 语句使得一个协程在多个通讯操作上等待。类似于 switch 语句。select 语句的每个 case 必须是一个通信操作,要么是发送要么是接收。
select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。
- 每个 case 都必须是一个通信;
- 所有管道表达式都会被求值;
- 所有被发送的表达式都会被求值;
- 如果任意某个通信可以进行,它就执行,其他被忽略;
- 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行;
- 如果所有 case 都不可执行,如果有 default 子句,则执行该语句;
- 如果没有 default 字句,select 将阻塞,直到某个通信可以运行。
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x: // 当 push 到 c 中的值没有被 fmt.Println 消费掉的时候,会阻塞
x, y = y, x+y
case <-quit: // quit 管道一直为空,阻塞,直到 go func() 协程循环结束
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int) // c 的长度只有 1 个 int 值
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
5.4. 互斥
Go 标准库中提供了 sync.Mutex
类型及 Lock
和 Unlock
个方法实现互斥。
通过在代码前调用 Lock
方法,在代码后调用 Unlock
方法来保证一段代码的互斥执行。用 defer
语句来保证互斥锁一定会被解锁。
type SafeCounter struct {
v map[string]int // 数据对象
mux sync.Mutex // 互斥锁
}
// 计数器
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
c.v[key]++ // 每次只有一个协程可以访问此代码
c.mux.Unlock()
}
// 返回计数值
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
defer c.mux.Unlock() // defer 确保解锁在数据返回后执行
return c.v[key]
}