Go语言入门
本文算是《Go语言实战》读书笔记,同时也写了一些go
与java
中的不同之处。
简单的例子
1 | package main |
代码结构
包(Package)
package
的定义和java类似,按照惯例包名和文件夹同名,但是如果生成可执行文件的话,则main
函数包名必须为main
导入(Import)
import
的定义和java类似,用于导入外部库。与java不同的是,在go中声明的库必须是要被使用的,如果仅仅想声明但是不想使用的话可以使用_
标明,并且go允许用户导入网络上的库:
1 | import ( |
init()函数
每个包可以包含多个init
函数,每个init
函数都会在程序执行开始的时候被调用,都会安排在main
函数之前执行。init
函数用户设置包、初始化变量或者用于在程序运行前完成初始化工作
使用大小写标记访问权限
go中没有类似java中的public
,private
等标明访问权限关键字,而使用变量或者函数是否以大写字符开头标记。使用大写字符开头的变量或函数是可以被其他包访问,而小写的则是不可被其他包访问
空白标识符
下划线字符(_
)在go中称为空白标识符,用于标记不想使用的值,如在多个返回值的方法中,不想关注的返回值可以使用空白标识符标记,例如打开文件方法中需要接收error返回值:
1 | f, err := 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 | var a int32 = 13 |
数字转换
数字之间转换(如int,float)都可以使用 type(num)
这种方式,如:
1 | // int64转int |
string转int
1 | var str = "123" |
string转int64
1 | var str = "123" |
int转string
1 | var i = 123 |
int64转string
1 | var i int64 = 123 |
string转float32
1 | var str = "1.23" |
string转float64
1 | var str = "1.23" |
string与byte[]
1 | var str = "string" |
指针
java中没有指针的概念
1 | var i = 100 |
可以简单的理解为变量前加上&,即表示获取该变量的内存地址,而使用 *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 | array1 := [5]int{10, 20, 30, 40} |
但是如果是指针复制,那么复制的是指针,而非指针指向的值(类似java中的=
复制):
1 | array1 := [1]*int{new(int)} |
截取
数组截取与复制不同,截取之后的新数组底层使用的还是原数组,因此如果修改了新数组的元素,原数组也会有影响(其实数组的截取是切片的截取):
1 | array3 := [5]int{1, 2, 3, 4, 5} |
函数间传递
在函数间传递数组是一个开销很大的操作,因为数组是以值的方式传递,意味着每次调用函数,都会将数组复制,因此如果不在函数中修改数组的话,推荐使用指针的方式传递数组:
1 | array := [5]int{1, 2, 3, 4, 5} |
切片
go中的切片可以看做是可以动态扩充的数组,有长度和容量的概念,和java中的ArrayList
类似
创建和初始化
创建切片的方式可以通过make
函数:
1 | slice := make([]int, 5) // 长度和容量都为5的切片 |
声明数组的方式也适用于切片,如:
1 | slice2 := []int{1, 2, 6, 7, 5, 6} |
声明数组和切片最大的不同:初始化时未指定 [] 中的值,为初始化切片;指定了即为初始化数组
1 | array := [6]int{1, 2, 6, 7, 5, 6} // 定义了长度为6的数组 |
创建 nil 切片:
1 | var slice []int |
创建空切片:
1 | // 使用 make 创建空的整型切片 |
不管是使用 nil 切片还是空切片,都是支持内置方法调用的。
截取
其实上面数组的截取就是切片的截取,切片截取后,新的切片指向的是原切片的地址,也就是新切片和原切片指向的是同一底层数组,因此在原切片的容量内,新切片操作和原切片的操作会相互影响:
1 | // 声明长度为3 容量为4的切片 初始值为默认,即[0,0,0] |
长度与容量计算:对底层数组容量是k
的切片slice[i:j]
来说,长度为j-i
,容量为k-i
除了使用slice[i:j]
来截取切片,还可以使用slice[i:j:k]
,在这种情况下,k
为新切片的容量,长度和容量计算方式和上面一致,在这种情况下,如果我们将k
的值设置为何j
相同,那么就可以避免对新切片的append
操作影响到老切片(当append
操作超过切片容量是,会创建新切片):
1 | slice := make([]int, 3, 4) |
函数间传递
与数组不同,切片在函数间的传递不会复制底层数组,因此可以直接以值的方式传递:
1 | slice := make([]int, 3, 4) |
迭代
数组和切片的迭代方式相同,使用for .. range
:
1 | array3 := [5]int{1, 2, 3, 4, 5} |
可以看到这种情况下range
创建了每个元素的副本,而不是直接返回对该元素的引用。
当然我们也可以使用传统的for
循环:
1 | for i := 0; i < len(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 | // 定义key为string,value为int |
函数间传递
映射在函数间传递和切片类似,传递的都是引用,即在函数中对映射的所有修改,对这个映射的引用都可见
结构体
创建一个结构类型
结构体的声明格式为type 名称 struct
,如:
1 | type Person struct { |
和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 | bill := Person{age: 11, name: "bill", address: Address{"zhejiang"}} |
接口
go的接口和java类似,只是不需要显示定义,如果类型(基本类型,结构体等都可以)拥有了接口的所有方法,那么就默认为这个类型实现了这个接口
接口的声明格式为type 名称 interface
,如:
1 | // 声明一个接口 |
嵌入类型
go中的结构体没有继承的概念,需要使用嵌入类型完成类似java中的继承
1 | type user struct { |
和java中的继承方法调用类似,当子类覆盖了父类的方法,执行子类的方法,否则执行父类方法:
1 | func main() { |
并发
go中并发指的是能让某个函数独立于其他函数运行的能力,类似java中的Thread
。如果要让函数独立于其他函数运行,使用关键字go
:
1 | func main() { |
在指定单个处理器的情况下,有可能看到的情况是大小写字母混合输出,这是因为这种情况下,调度器会切换不同的goroutine
(如果使用耗时较长的操作会更加明显的看到效果)
并发与并行
并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做 了一半就被暂停去做别的事情了。在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。
sync.WaitGroup
计数信号量,类似java中的Semaphore
(例子中可能用CountDownLatch
类比会更加合适一点),可以用来记录并维护运行的goroutine。
defer
关键字defer
会修改函数调用时机,在执行函数返回时在会调用defer
声明的函数,类似java中finally
关键字
原子函数(Atomic)
和java中的原子类(如AtomicInteger
)类似,go里也提供了原子函数(sync/atomic
)保证了原子操作:
1 | counter int64 |
互斥锁(Mutex)
go中的互斥锁的用法和java中的Lock
类似:
1 | // 声明 |
通道(channel)
通道用于在不同的goroutine
之间发送和接收需要共享的资源
无缓冲和有缓冲
从生产者-消费者来说,无缓冲就是生产者和消费者必须同时准备好,生产者生产,消费者必须立刻消费;而有缓冲提供了一个存储区,在存储区满之前,生产者可以不等消费者继续生产
创建和使用
通道使用关键字make
创建,类型为chan
:
1 | // 无缓冲的整型通道 |
往通道中发送和接收数据,使用操作符<-
:
1 | // 往通道中发送数据 |
关闭通道使用close()
函数,close()
函数调用后,无法再往通道发送数据,当通道的数据全部被接收后,通道才会关闭:
1 | unbuffered := make(chan int) |
select-case
select
语法和switch
类似,只是case
涉及到通道有关的I/O操作:
1 | select { |
日志
1 | var ( |
JSON编码/解码
从JSON
字符串到对象使用json.Unmarshal
,从对象到JSON
字符串使用json.Marshal
:
1 | func main() { |
Writer和Reader
io.Writer接口
1 | // Write 从 p 里向底层的数据流写入 len(p)字节的数据。 |
Write
方法的实现需要试图写入传入的byte切片
里的所有数据。但是,如果无法全部写入,那么就会返回错误,返回的写入字节数可能会小于byte切片
的长度。最重要的是写入时绝对不能修改byte切片
里的数据。
io.Reader接口
1 | // (1) Read最多读入len(p)字节,保存到p。这个方法返回读入的字节数(0 <= n <= len(p))和任何读取时发生的错误。即便Read返回的n < len(p),方法也可 能使用所有 p 的空间存储临时数据。如果数据可以读取,但是字节长度不足 len(p), 习惯上 Read 会立刻返回可用的数据,而不等待更多的数据。 |
简单读写文件
1 | func main() { |