Go语言入门

2019

本文算是《Go语言实战》读书笔记,同时也写了一些gojava中的不同之处。

简单的例子

1
2
3
4
5
6
7
8
package main

import "fmt"

// main 是整个程序入口
func main() {
fmt.Println("Hello World!")
}

代码结构

包(Package)

package的定义和java类似,按照惯例包名和文件夹同名,但是如果生成可执行文件的话,则main函数包名必须为main

导入(Import)

import的定义和java类似,用于导入外部库。与java不同的是,在go中声明的库必须是要被使用的,如果仅仅想声明但是不想使用的话可以使用_标明,并且go允许用户导入网络上的库:

1
2
3
4
5
6
import (
"fmt"
_"github.com/aws/aws-lambda-go/lambda" // 声明库但是不使用,但是仍会调用init方法(如果存在)
"github.com/aws/aws-sdk-go/aws" // 使用github上的库
myfmt "mylib/fmt" // 用于引入相同名称的库
)

init()函数

每个包可以包含多个init函数,每个init函数都会在程序执行开始的时候被调用,都会安排在main函数之前执行。init函数用户设置包、初始化变量或者用于在程序运行前完成初始化工作

使用大小写标记访问权限

go中没有类似java中的publicprivate等标明访问权限关键字,而使用变量或者函数是否以大写字符开头标记。使用大写字符开头的变量或函数是可以被其他包访问,而小写的则是不可被其他包访问

空白标识符

下划线字符(_)在go中称为空白标识符,用于标记不想使用的值,如在多个返回值的方法中,不想关注的返回值可以使用空白标识符标记,例如打开文件方法中需要接收error返回值:

1
2
3
f, err := os.Open("/User/abc.jpg") // 接收并处理error

f, _ := os.Open("/User/abc.jpg") // 不关注error

命名规则

变量

与java不同,go声明变量的规则为var 变量 类型 = 值,如:

1
var name string = "Alan"

也可以省略类型,如:

1
var name = "Alan"

也可以使用简单声明方式:=,如:

1
name := "Alan"

常量

java中的常量声明:

1
private static final String ACCESS_KEY = "AK";

go中的常量声明则简单许多,使用const

1
const gender string = "male"

常用的类型转换

go中不同类型之间不能使用运算符,如:

1
2
3
4
var a int32 = 13
var b int64 = 20

c := int64(a) + b // 需要类型转换

数字转换

数字之间转换(如int,float)都可以使用 type(num)这种方式,如:

1
2
3
4
5
6
7
8
9
10
11
// int64转int
var i = 32
var i64 = int64(i)

// int转int64
var i int64 = 32
var i32 = int(i)

// float转int
var f = 134.73
var i = int(f)

string转int

1
2
var str = "123"
strInt, _ := strconv.Atoi(str)

string转int64

1
2
var str = "123"
strInt, _ := strconv.ParseInt(str, 10, 64)

int转string

1
2
var i = 123
str := strconv.Itoa(i)

int64转string

1
2
var i int64 = 123
str := strconv.FormatInt(i, 10)

string转float32

1
2
var str = "1.23"
f, _ := strconv.ParseFloat(str, 32)

string转float64

1
2
var str = "1.23"
f, _ := strconv.ParseFloat(str, 64)

string与byte[]

1
2
3
4
var str = "string"
bs := []byte(str) // string -> byte[]

str2 := string(bs) // byte[] -> string

指针

java中没有指针的概念

1
2
3
4
5
var i = 100
var p = &i //&i 表示取i所在的内存地址
fmt.Println(*p) // *p 表示指向i的内存地址的指针,获取该内存上的值.输出100
*p = 101 // *p 此处修改了i的内存的值
fmt.Println(*p, i) // 101 101

可以简单的理解为变量前加上&,即表示获取该变量的内存地址,而使用 *p 则是代表了指向该内存的指针,可以获取该内存地址上的值,如果修改了 *p 的值,那么原变量的值也会被修改。

数组、切片、映射

数组

go中的数组定义与java中的类似

创建和初始化

声明一个数组,并且使用默认值:

1
var array [5]int // 长度为5 初始值为0的数组

也可以在声明数组时同时赋值:

1
array := [5]int{10, 20, 30, 40} // 长度为5 第5个元素为0的数组 内容[10 20 30 40 0]

如果只想指定索引处的值:

1
array := [5]int{2: 30} // 长度为5 第3个元素为30 内容[0 0 30 0 0]

也可以在不声明数组长度,数组长度由初始化值的长度决定,可以使用...代替数组长度:

1
array := [...]int{10, 20, 30, 40, 50} // 长度为5的数组

复制

数组复制与java不同,java复制数组不能使用=,需要使用类似Arryas.copyOf()方法;而go中可以直接使用=进行复制:

1
2
3
4
5
6
array1 := [5]int{10, 20, 30, 40}
array2 := array1
fmt.Println(array1, array2) // [10 20 30 40 0] [10 20 30 40 0]

array2[0] = 20
fmt.Println(array1, array2) // [10 20 30 40 0] [20 20 30 40 0]

但是如果是指针复制,那么复制的是指针,而非指针指向的值(类似java中的=复制):

1
2
3
4
5
6
7
array1 := [1]*int{new(int)}
*array1[0] = 1
array2 := array1
fmt.Println(*array1[0], *array2[0]) // 1 1

*array2[0] = 100
fmt.Println(*array1[0], *array2[0]) // 100 100

截取

数组截取与复制不同,截取之后的新数组底层使用的还是原数组,因此如果修改了新数组的元素,原数组也会有影响(其实数组的截取是切片的截取):

1
2
3
4
5
6
array3 := [5]int{1, 2, 3, 4, 5}
array4 := array3[1:4]
fmt.Println(array4) // [2 3 4]

array4[0] = 1
fmt.Println(array4, array3) // [1 3 4] [1 1 3 4 5]

函数间传递

在函数间传递数组是一个开销很大的操作,因为数组是以值的方式传递,意味着每次调用函数,都会将数组复制,因此如果不在函数中修改数组的话,推荐使用指针的方式传递数组:

1
2
3
4
5
6
array := [5]int{1, 2, 3, 4, 5}
find(&array)

func find(array *[5]int){
// ...
}

切片

go中的切片可以看做是可以动态扩充的数组,有长度和容量的概念,和java中的ArrayList类似

创建和初始化

创建切片的方式可以通过make函数:

1
2
3
slice := make([]int, 5) // 长度和容量都为5的切片

slice := make([]int, 3, 5) // 长度为3 容量为5的切片

声明数组的方式也适用于切片,如:

1
slice2 := []int{1, 2, 6, 7, 5, 6} 

声明数组和切片最大的不同:初始化时未指定 [] 中的值,为初始化切片;指定了即为初始化数组

1
2
array := [6]int{1, 2, 6, 7, 5, 6} // 定义了长度为6的数组
slice := []int{1, 2, 6, 7, 5, 6} // 定义了长度和容量都为6的切片

创建 nil 切片:

1
var slice []int

创建空切片:

1
2
3
4
// 使用 make 创建空的整型切片
slice := make([]int, 0)
// 使用切片字面量创建空的整型切片
slice := []int{}

不管是使用 nil 切片还是空切片,都是支持内置方法调用的。

截取

其实上面数组的截取就是切片的截取,切片截取后,新的切片指向的是原切片的地址,也就是新切片和原切片指向的是同一底层数组,因此在原切片的容量内,新切片操作和原切片的操作会相互影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 声明长度为3 容量为4的切片 初始值为默认,即[0,0,0]
slice := make([]int, 3, 4)
// 从原切片中截取创建新切片 长度为2 容量为3 即[0,0]
slice2 := slice[1:3]
fmt.Println(slice, slice2) // [0 0 0] [0 0]

// 对slice2索引为0的元素赋值,因为是从原切片中截取,所以修改了slice[1]
slice2[0] = 1
fmt.Println(slice, slice2) // [0 1 0] [1 0]

// 对slice2添加元素,因为slice长度为3,所以看不出来
slice2 = append(slice2, 24)
fmt.Println(slice, slice2) // [0 1 0] [1 0 24]

// 对slice添加元素,因为还在slice容量内,所以slice2也会被影响
slice = append(slice, 4)
fmt.Println(slice, slice2) // [0 1 0 4] [1 0 4]

// 对slice2添加元素,因为已经超过了原切片的容量,此时其实是新的切片 长度为4 容量为6
slice2 = append(slice2, 25)
fmt.Println(slice, slice2) // [0 1 0 4] [1 0 4 25]

// 上一步中slice2已经是新的切片,所以对slice的改动不会影响到slice2
slice = append(slice, 5)
fmt.Println(slice, slice2) // [0 1 0 4 5] [1 0 4 25]

slice2[0] = 111
fmt.Println(slice, slice2) // [0 1 0 4 5] [111 0 4 25]

长度与容量计算:对底层数组容量是k的切片slice[i:j]来说,长度为j-i,容量为k-i

除了使用slice[i:j]来截取切片,还可以使用slice[i:j:k],在这种情况下,k为新切片的容量,长度和容量计算方式和上面一致,在这种情况下,如果我们将k的值设置为何j相同,那么就可以避免对新切片的append操作影响到老切片(当append操作超过切片容量是,会创建新切片):

1
2
3
4
5
6
7
8
9
10
11
slice := make([]int, 3, 4)
// 新切片长度为2 容量为2
slice2 := slice[1:3:3]
fmt.Println(slice, slice2) // [0 0 0] [0 0]

// 超过容量创建新切片,此时和原切片是2个不同的底层数组
slice2 = append(slice2, 3)
fmt.Println(slice, slice2) // [0 0 0] [0 0 3]

slice[2] = 1
fmt.Println(slice, slice2) // [0 0 1] [0 0 3]

函数间传递

与数组不同,切片在函数间的传递不会复制底层数组,因此可以直接以值的方式传递:

1
2
3
4
5
6
slice := make([]int, 3, 4)
find(slice)

func find(slice []int) {
// ...
}

迭代

数组和切片的迭代方式相同,使用for .. range

1
2
3
4
5
6
7
8
9
10
11
array3 := [5]int{1, 2, 3, 4, 5}

for i, e := range array3 { // i为索引值,e为对应的元素
fmt.Println(i, e, &e, &array3[i])
}

//0 1 0xc000094090 0xc00008e180
//1 1 0xc000094090 0xc00008e188
//2 3 0xc000094090 0xc00008e190
//3 4 0xc000094090 0xc00008e198
//4 10 0xc000094090 0xc00008e1a0

可以看到这种情况下range创建了每个元素的副本,而不是直接返回对该元素的引用。

当然我们也可以使用传统的for循环:

1
2
3
for i := 0; i < len(array3); i++ {
fmt.Println(i, array3[i])
}

映射

go中的映射类似java中HashMap

创建和初始化

映射可以使用make函数创建:

1
m := make(map[int]string) // 定义key为int,value为string

也可以直接赋值创建:

1
m := map[string]int{"k1": 1, "k2": 2} // 定义key为string,value为int

基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义key为string,value为int
m := map[string]int{"k1": 1, "k2": 2}

// v1为'k1'的值,不存在时返回默认值;contains为bool类型(可选):是否存在
v1, contains := m["k3"]
fmt.Println(v1, contains)

// 赋值操作
m["k2"] = 22

// 删除指定的key
delete(m, "k2")

// 循环输出所有的kv
for k, v := range m {
fmt.Println(k, v)
}

函数间传递

映射在函数间传递和切片类似,传递的都是引用,即在函数中对映射的所有修改,对这个映射的引用都可见

结构体

创建一个结构类型

结构体的声明格式为type 名称 struct,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Person struct {
age int
name string
address Address
}

type Address struct {
city string
}

// 方法 使用指针接收
func (p *Person) GetName() string {
return p.name
}

// 方法 使用值接收
func (p Person) SetAge(age int) {
p.age = age
}

// 方法 使用指针接收
func (p *Person) SetAgePoint(age int) {
p.age = age
}

// 方法 使用值接收
func (p Person) introduce() {
fmt.Printf("My Name is %s. I'm %d. I'm from %s.\n", p.name, p.age, p.address.city)
}

和java中的class不同,go中的struct只能定义属性,不能定义方法。如果要定义方法,需要在关键字func和方法名之间增加结构类型参数。

方法其实也是函数,关键字func和函数名之间的参数被称作接收者,将函数与接收者的类型绑在一起。如果一个函数有接收者,这个函数就被称为方法

go中方法接收者分为值接收者指针接收者,虽然在调用上可以互相调用,但值接收者的调用是结构体的副本,而指针接收者调用的是指向结构体的指针,也就是值接收者的调用不会修改结构体本身,而指针接收者的调用会影响结构体。简而言之,如果是要创建新值,就用值接收者,如果是修改当前的值,就用指针接收者

使用结构类型

声明一个Person类型的变量:

1
var bill Person

也可以在声明的同时初始化属性值:

1
bill := Person{11, "bill", Address{"zj"}}

也可以指定属性值创建:

1
bill := Person{age: 11, name: "bill"}

调用结构方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bill := Person{age: 11, name: "bill", address: Address{"zhejiang"}}
bill.introduce() // My Name is bill. I'm 11. I'm from zhejiang.
// 值调用 不影响结构体
bill.SetAge(12)
bill.introduce() // My Name is bill. I'm 11. I'm from zhejiang.
// 指针调用 影响结构体
bill.SetAgePoint(13)
bill.introduce() // My Name is bill. I'm 13. I'm from zhejiang.

lisa := &Person{14, "Lisa", Address{"shanghai"}}
lisa.introduce() // My Name is Lisa. I'm 14. I'm from shanghai.
// 值调用 不影响结构体
lisa.SetAge(15)
lisa.introduce() // My Name is Lisa. I'm 14. I'm from shanghai.
// 指针调用 影响结构体
lisa.SetAgePoint(16)
lisa.introduce() // My Name is Lisa. I'm 16. I'm from shanghai.

接口

go的接口和java类似,只是不需要显示定义,如果类型(基本类型,结构体等都可以)拥有了接口的所有方法,那么就默认为这个类型实现了这个接口

接口的声明格式为type 名称 interface,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 声明一个接口
type notifier interface {
notify()
}

type user struct {
name string
age int
}

// 接口实现方法 此处不能使用指针接收者
func (u user) notify() {
fmt.Printf("My Name is %s. I'm %d.\n", u.name, u.age)
}

// 接口实现方法 使用指针接收者 调用时需要传入user的指针
//func (u *user) notify() {
// fmt.Printf("My Name is %s. I'm %d.\n", u.name, u.age)
//}

// 调用接口函数
func sendNotify(n notifier) {
n.notify()
}

func main() {
bill := user{"bill", 11}
sendNotify(bill)
// sendNotify(&bill) 传入user地址,调用指针方法
}

嵌入类型

go中的结构体没有继承的概念,需要使用嵌入类型完成类似java中的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type user struct {
name string
age int
}

// 类似admin 继承 user
type admin struct {
user
level int
}

func (u user) notify() {
fmt.Printf("My Name is %s. I'm %d.\n", u.name, u.age)
}

func (a admin) notify() {
fmt.Printf("My Name is %s. I'm %d. My level is %d.\n", a.name, a.age, a.level)
}

和java中的继承方法调用类似,当子类覆盖了父类的方法,执行子类的方法,否则执行父类方法:

1
2
3
4
5
6
7
8
func main() {
bill := admin{user{"bill", 11}, 1}
// 调用admin方法,如admin未实现方法,则调用user.notify
bill.notify() // My Name is bill. I'm 11. My level is 1.

// 调用父类方法
bill.user.notify() // My Name is bill. I'm 11.
}

并发

go中并发指的是能让某个函数独立于其他函数运行的能力,类似java中的Thread。如果要让函数独立于其他函数运行,使用关键字go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func main() {

// 指定只使用一个处理器
//runtime.GOMAXPROCS(1)

var wg sync.WaitGroup
wg.Add(2)

fmt.Println("start...")

go func() {
defer wg.Done()
// 输出小写字母
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c ", char)
}
}()

go func() {
defer wg.Done()
// 输出大写字母
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c ", char)
}
}()

fmt.Println("waiting...")
// 阻塞 等待goroutine结束
wg.Wait()
fmt.Println("end...")
}

在指定单个处理器的情况下,有可能看到的情况是大小写字母混合输出,这是因为这种情况下,调度器会切换不同的goroutine(如果使用耗时较长的操作会更加明显的看到效果)

并发与并行

并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做 了一半就被暂停去做别的事情了。在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。

sync.WaitGroup

计数信号量,类似java中的Semaphore(例子中可能用CountDownLatch类比会更加合适一点),可以用来记录并维护运行的goroutine。

defer

关键字defer会修改函数调用时机,在执行函数返回时在会调用defer声明的函数,类似java中finally关键字

原子函数(Atomic)

和java中的原子类(如AtomicInteger)类似,go里也提供了原子函数(sync/atomic)保证了原子操作:

1
2
3
4
5
6
7
counter int64
// 原子加操作
atomic.AddInt64(&counter, 1)
// 赋值
atomic.StoreInt64(&counter, 14)
// 取值
atomic.LoadInt64(&counter)

互斥锁(Mutex)

go中的互斥锁的用法和java中的Lock类似:

1
2
3
4
5
6
7
8
// 声明
mutex sync.Mutex

mutex.Lock()
{
// do something
}
mutex.Unlock()

通道(channel)

通道用于在不同的goroutine之间发送和接收需要共享的资源

无缓冲和有缓冲

从生产者-消费者来说,无缓冲就是生产者和消费者必须同时准备好,生产者生产,消费者必须立刻消费;而有缓冲提供了一个存储区,在存储区满之前,生产者可以不等消费者继续生产

创建和使用

通道使用关键字make创建,类型为chan

1
2
3
4
5
// 无缓冲的整型通道
unbuffered := make(chan int)

// 有缓冲的字符串通道,缓冲区大小为10
buffered := make(chan string, 10)

往通道中发送和接收数据,使用操作符<-

1
2
3
4
// 往通道中发送数据
unbuffered <- 1
// 从通道中接收数据
i,ok := <-unbuffered

关闭通道使用close()函数,close()函数调用后,无法再往通道发送数据,当通道的数据全部被接收后,通道才会关闭:

1
2
3
unbuffered := make(chan int)

close(unbuffered)

select-case

select语法和switch类似,只是case涉及到通道有关的I/O操作:

1
2
3
4
5
6
7
8
9
select {
case <-chan1:
// 当chan1成功发送数据是时执行
case chan2 <- 1:
// 当chan2成功接收数据是时执行
default:
// 如case中的情况都不满足,默认进入default
// 如果没有default语句并且没有case满足条件时,select会被阻塞
}

日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var (
Error *log.Logger
Info *log.Logger
)

func init() {
file, err := os.OpenFile("error.log",
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalln("Failed to open error log file:", err)
}

// 输出到控制台
Info = log.New(os.Stdout, "INFO: ", log.Ldate|log.Lmicroseconds|log.Llongfile)

// 输出到文件
Error = log.New(io.MultiWriter(file, os.Stderr), "ERROR: ", log.Ldate|log.Lmicroseconds|log.Llongfile)
}

func main() {
Info.Println("Special Information")
Error.Println("Something has failed")
}

JSON编码/解码

JSON字符串到对象使用json.Unmarshal,从对象到JSON字符串使用json.Marshal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
var r room
jsonString := `{"roomId":1234, "name":"LiveRoom"}`

// json字符串到对象
e := json.Unmarshal([]byte(jsonString), &r)
if e != nil {
fmt.Println(e)
return
}
fmt.Println(r)

// 对象到json字符串
b, _ := json.Marshal(r)
fmt.Println(string(b))
}

Writer和Reader

io.Writer接口

1
2
3
4
5
6
7
// Write 从 p 里向底层的数据流写入 len(p)字节的数据。
// 这个方法返回从 p 里写出的字节 数(0 <= n <= len(p)),以及任何可能导致写入提前结束的错误。
// Write在返回n < len(p)的时候,必须返回某个非nil值的error。
// Write绝不能改写切片里的数据, 哪怕是临时修改也不行。
type Writer interface {
Write(p []byte) (n int, err error)
}

Write方法的实现需要试图写入传入的byte切片里的所有数据。但是,如果无法全部写入,那么就会返回错误,返回的写入字节数可能会小于byte切片的长度。最重要的是写入时绝对不能修改byte切片里的数据。

io.Reader接口

1
2
3
4
5
6
7
// (1) Read最多读入len(p)字节,保存到p。这个方法返回读入的字节数(0 <= n <= len(p))和任何读取时发生的错误。即便Read返回的n < len(p),方法也可 能使用所有 p 的空间存储临时数据。如果数据可以读取,但是字节长度不足 len(p), 习惯上 Read 会立刻返回可用的数据,而不等待更多的数据。
// (2) 当成功读取 n > 0字节后,如果遇到错误或者文件读取完成,Read方法会返回 读入的字节数。方法可能会在本次调用返回一个非 nil 的错误,或者在下一次调用时返 回错误(同时n == 0)。这种情况的的一个例子是,在输入的流结束时,Read会返回 非零的读取字节数,可能会返回err == EOF,也可能会返回err == nil。无论如何, 下一次调用Read应该返回0, EOF。
// (3) 调用者在返回的n > 0时,总应该先处理读入的数据,再处理错误err。这样才 能正确操作读取一部分字节后发生的 I/O 错误。EOF 也要这样处理。
// (4) Read的实现不鼓励返回0个读取字节的同时,返回nil值的错误。调用者需要将 这种返回状态视为没有做任何操作,而不是遇到读取结束。
type Reader interface {
Read(p []byte) (n int, err error)
}

简单读写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {

writeToFile("test.txt")

readFromFile("test.txt")
}

func writeToFile(fileName string) {
// 创建文件
f, _ := os.Create(fileName)
defer f.Close()

n, _ := io.WriteString(f, "ABCXYZ") //写入字符串
fmt.Printf("写入字节数:%d \n", n)
}

func readFromFile(fileName string) {
file, _ := os.Open(fileName)
defer file.Close()

fileInfo, _ := file.Stat()

// 全部读取
buf := make([]byte, fileInfo.Size())
file.Read(buf)

fmt.Println(string(buf))
}

参考

go语言实战随书代码