Go常用语法
开始正式学习 Go,参考该网站 link。
前言
Import
1 | package main |
类似 C 语言,Go 程序通过调起各种包(Packages)运行,在 Import 中是调起的其他包的路径。除此之外,Go 默认也是从 main 开始运行,所以 main 函数仍然是不可缺少的。
为引入这些包,需要这样写
1 | import "fmt" |
Export
在 Go 中,大写字母开头的值是视为 Exported 的,可以为外界调用,反之则不是。
1 | package main |
例如在下述的程序中,math.pi就是不可调用的,并得到这样的报错:
1 | ./prog.go:9:14: cannot refer to unexported name math.pi |
Functions
函数定义基本和 C 语法相同,但仍存在一些差异:例如他的形参数据类型放置在形参名字后面,例如:
1 | func add(x int, y int) int { |
此外,当连续多个形参使用相同的数据类型时,可以省略除了该序列中最后一个外的所有数据类型声明,例如上面的例子可以重写为:
1 | func add(x, y int) int { |
定义多返回值的函数也是可以的,形式如下:
1 | func swap(x, y string) (string, string) { |
此外,可以在函数头上声明返回的值的 name,从而在 return 语句时不加强调。然而这种方法并未得到提倡,因为在很长的函数中,似乎可读性并不强。
1 | func split(sum int) (x, y int) { |
上述函数应当返回(7,10),即(x,y)对应的值。
Variables
使用 var 语句声明变量。声明的方法如同函数形参,例如:
1 | var c, python, java bool |
并且参数是可以定义在 package level 的,即上例中的 c, python…除了简单的声明变量,还可以初始化,变量的数据类型和初始化的数据相关。如下:
1 | var i, j int = 1, 2 |
如果未显式地声明,而仅仅是使用 var 定义,则同时定义不同的数据类型是可以接受的。此外可以使用:=代替这种情况。
数据结构
基本数据类型
1 | bool |
这些数据类型没有初始化时,会被赋给 0 值,如int对应 0,srting对应空字符串。
类型转换
类型转换使用a = T(b)完成,如下例:
1 | var i int = 42 |
在 Go 中,类型转换是必须显式声明的。
常量
常量的声明和普通变量相同,但是需要在开头加上一个 const。如下:
1 | const Pi = 3.14 |
需要注意的是,常量不可以使用:=定义。
指针
指针的形式和 C 相似,都是通过*T表示。定义如下:
1 | var *p int |
取地址方法及取值方法也和 C 相同。
Struct 语句
声明一个新类型方法如下:
1 | type Vertex struct { |
struct 中的值可以用.获得。也可以通过指针的方法:
1 | v := Vertex(1, 2) |
理论上需要通过(*p).X访问 X 值,然而 Go 允许,仅仅使用p.X直接对其进行访问。
数组
1 | var a [2]string |
Go 通过这种方式来定义数组。他的访问和赋值都和 C 相同。Array 的长度是固定的,不可以在运行过程中修改的。在上述例子中存在一个类似 C 的初始化方法,在这个初始化中,尽管声明了 6 个整形的空间,但仅仅给了 5 个初始值,则最后的一个元素会被初始化为 0。
Slide
与之相关的是一个特殊的数据结构 Slide:
1 | primes := [6]int{2, 3, 5, 7, 11, 13} |
这个的表现和 C 语言是完全一样的,然而和预期不同的是,这个 Slide 并不储存数据,而是仅仅类似于地址和指针一样的东西。对于 Slide 的更改会导致对于其截取的原数组的更改,并且其他包含相同元素的 Slide 也会立刻应用这些更改(因为他们只是取地址)。
对于 Slide 的元素截取和 Python 一样,可以使用:符号表示截取范围。
1 | var a [10]int |
以上四种表达是等价的。Slide 存在 length 和 capacity 两个变量。前者为 Slide 包含的元素数量,后者为 Slide 所指向的 Array,从 Slide 包含的第一个元素开始计算的元素数。这两个值分别可以通过函数len()及cap()获取。
特殊的是,我们可以 extend Slide 的范围。对于以下的语句:
1 | s := []int{2, 3, 5, 7, 11, 13} |
其并不是在 Slide s 中重新获得新的 Slide,而是在完成一次 Re-sldie。即s = s[:4]语句是针对最开始的长度为 6 的数组进行的。这仅仅发生在,数组的长度右端超过了 Slide 的长度并小于 Slide 的容量时才会发生,称为 Extend。
Nil Slide
空数组:
1 | var s []int |
Append
类似 Python 的 list,Go 提供了一个可变长的数组。这个数组在元素超出容量时会自动再分配一个空间,然后返回的地址指向一个新的数组。其使用如下:
1 | func main() { |
Range
类似 Python 的,他的 For 循环也可使用 Range 操作。Range 在每一个迭代返回一个计数器和一个对象对应的值。例如:
1 | var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} |
如果不希望获得对应的值,则可以使用_代替位置。如果只希望使用 index,则只显式地记下一个值即可。如:
1 | for i := range pow {} |
Map
Map 的 0 值为nil,一个 nil 的 Map 既没有 key 也不能增加新的 key。Map 可以使用 make 函数初始化:
1 | m = make(map[string]Vertex) |
其中,string 是键值,Vertex 是 Map 指向的对象。Map 的访问是通过键值访问的,这个设定与 C 及 Python 都一致。下面是一个更完整的示例:
1 | type Vertex struct { |
此外,Map 还可以如此初始化:
1 | var m = map[string]Vertex{ |
或者干脆省略 Vertex 声明,改为:
1 | var m = map[string]Vertex{ |
对 Map 的其他操作
1 | delete(m, key) // 从字典m中删去Key及其对应的值。 |
Closure
1 | func adder() func(int) int { |
在这个例子里,函数 adder()相当于一个“函数模型”,调用这个模型获得的是一个函数的实体即 pos, neg。而这个 sum 是这个函数模型实体的参数,所以会逐渐累加,其效果如同 C 中的 static 变量。
语法
For 语句
Go 仅含有这样一种循环语句。如下:
1 | for i := 0; i < 10; i++ { |
这个和 C 的语法非常像。同样的,如果不需要这三要素中的某一部分,可以完全空出来,如同 C 的操作。如上第二个或第三个 For 循环。而在 Go 中,并没有专门的 While 语句,有上述第三种 For 循环代替。
更直接的,如果希望写出一个死循环,则可以如此写:
1 | for { |
If 语句
和 For 语句一样,成分不需要使用括号包含。但是大括号是需要的,如下:
1 | if x < 0 { |
类似 For 语句,If 语句可以在条件前增加一个初始化语句。该初始化语句的内容,在后面大括号范围内有效。例如下面的写法是有效的:
1 | if v := math.Pow(x, n); v < lim { |
Switch 语句
Switch 语句和 C 语言相似,然而,Go 的 Switch 语句不会运行后面所有的部分,而仅仅运行满足条件的语句。如下:
1 | switch os := runtime.GOOS; os { |
以上的程序仅会运行其中的一项,而非所有。同时这个初始化语句和 If 语句相同,是可以省略的。与 C 不同的是,这里的 Case 语句不需要是 Const,同时也不需要是整型。同时,Switch 是从上往下执行的,他会在任何一个满足条件的 Case 中停下并不再考察后面的 Case。
特殊的,我们可以声明一个不带有条件的 Switch 语句。此时,这个 Switch 语句的含义是传递一个 True 值。然而我们在 Case 的声明时,仅需要返回 true 或者 false 即可。换句话说,我们可以将其作为一个 if-then-else 的链使用。如下:
1 | t := time.Now() |
Defer 语句
Defer 语句会暂停现在的所有执行,直到它环境里的其他语句执行结束后才会执行(即一个栈)。并且按照后定义的 Defer 先执行的顺序执行。
1 | func main() { |
例如上式的输出为hello \n !! \n world。
Methods
Go 没有类,但是可以在类上定义方法。这类方法的定义和其他函数有些许不同,需要在func关键词到方法名间增加一个 receiver。形式如下:
1 | type Vertex struct { |
这个方法也能为仅仅数据类型构造 Methods,然而它不能为其他 package 里的数据类型如此操作,或者对内建数据类型数次操作。如需要针对内建数据类型,则需要重定义:
1 | type MyFloat float64 |
然而,使用这样的 Receiver,不能对其含有的值进行操作。在我们需要操作其内容的时候,我们需要使用指针的 Receiver,如下:
1 | func (v Vertex) Abs() float64 { |
对于 Methods 来说,使用实体或者指向实体的指针操作实体中的参数都是可以的。
Interface
Interface 一种特殊的数据类型,它是一系列 Methods 签名的集合。如下:
1 | type Abser interface { |
我们说一个 Type 实现了一个 Interface,如果它存在 Interface 中声明的函数的实现。这个实现不需要显式地声明,他们的名称相同即可。
1 | type I interface { |
在上述例子中,存在 Interface I,Type T。其中 T 实现了 Methods M,则可以说是 Type T 实现了 Interface I。这样的 Interface 让实现和使用解耦,我们只需要关心 Interface 或者其实现,而不需要两者兼顾。
从更根本的眼光来看,Interface 是一个元组:(value, type)。它保存一个类型及其对应的值。对 Interface 调用一个 Method,相当于调用其代表的 type 对应的 Method。
关于 nil 值
存在这样的情况:Interface 对应的值不存在。但是这种情况的 Interface 并不为空,其保存了对应的类型。
1 | func (t *T) M() { |
在上面的例子中,t 并没有被初始化,所以对应的值是 nil 的(因为它甚至只是一个指针。)。而若去掉代码i = t,则 Interface i 就是一个空 Interface,此时调用M()就会报错。
关于空 Interface
对于没有声明 Methods 的 Interface 被称为“Empty Interface”。这样的 Interface 可以指向任何一个数据类型。这样的情况被用在处理不确定数据类型的时候。
关于 Interface 的 Type
我们有的时候需要知道 Interface 指向的 Type 是什么,此时我们这样调用:
1 | t := i.(T) |
如果 Type T 和 Interface i 的值完全相同,则会返回 i 对应的实体。如果类型不同则会报错。为了得知类型的同时不 raise error,我们采用下面的方法:
1 | t, ok := i.(T) |
如果 ok 为真,则意味着两种数据类型相同,并返回值 t;若为 false,则数据类型不同,且会返回 T 的 0 值。在这个设定下,我们可以这样写:
1 | func do(i interface{}) { |
这个语句比较 v 的类型。
Error
Error 是一类内建的 Interface。
1 | type error interface { |
所有的函数都会返回一个 error 值,若 error 值等于 nil,则表示成功运行。他可以像上述代码的后半部分那样使用。
Concurrency
Goroutines
一个 Goroutines 是 Go 驱动的一个线程。以下语句会调起一个新线程。
1 | go f(x, y, z) |
f, x, y, z 的检验都是发现在当前的 goroutine 的,而调起的新函数则会运行在一个新的 goroutine 中。不同的 goroutine 运行在一个相同的地址空间中,所以他们的值是共享的。
Channels
Channels 是一种类型相关的导管。我们可以通过这个东西接受或者发送值,通过运算符:<-。类似下面的写法:
1 | ch <- v // Send v to channel ch. |
Channels 的定义方法和 Map 及 Slide 类似,需要借助 make 函数,定义方法如下:
1 | ch := make(chan int) |
默认的情况下,这两个操作都会阻塞当前的 Channels。这样可以完成同步,并不需要显式地锁住线程。下面是一个示例:
1 | func sum(s []int, c chan int) { |
Buffered Channels
Channels 可以存在 Buffer。通过下面的定义方法:
1 | ch := make(chan int, 100) |
以上定义了 100 个 Buffer。Send 命令会 Block 仅有可能为 Buffer 已满;Receive 命令会 Block 仅有可能为 Buffer 为空。这两种情况会导致死锁错误。
close
Send 的过程中,可以主动关闭 Channel,即不再发送数据;Receive 可以检验某个 Channel 是否被关闭。如下:
1 | func fibonacci(n int, c chan int) { |
事实上,Close 在这里不是必要的,除非 Receiver 需要明确地知道数据发送已经结束并终结进程。