今天跟着golang tour 了解了一下golang,记录一下这门语言的一些奇怪特性
(其实就是特性,因为脑子里在酝酿《perl的奇怪特性》这篇文章,所以用相
似的标题)

语言特点

导出名大写

包中导出函数都是大写的,小写的函数不会被导出。用这种简洁的方法
实现了C++中的public与private的作用。

语句末尾没有分号

许多现代语言都已经省略分号了。值得一提的是,javascript也是可以
省略分号的。但有些公司或项目的规范中必须要求有分号,有些则要求必
须省略(vue.js)

类型在变量名之后

代码示例:

1
2
3
func add(x int,y int)int{
return x + y
}

为什么这样设计呢?
在这篇文章中有详细解释:Go’s Declaration Syntax

总结起来就是c风格的函数指针太难读了,比如:

1
int (*fp)(int a, int b);

这是一个函数的指针,它有两个int型的参数,并且返回int。参数的
名字是可以省略的,只留下类型:

1
int (*fp)(int, int);

但是,如果函数的参数也是一个函数呢?

1
int(*fp)(int(*ff)(int x,int y),int b)

这个就很难读了。
因为参数的名字是可省的,那么这个作为参数的函数的名字也可省,这样
这个函数就变成了这样:

1
int(*fp)(int(*)(int,int),int b)

WTF 我该把名字补充到哪里去呢?

更加抓狂的例子,如果一个函数的返回值类型是一个函数的指针呢?

1
int (*(*fp)(int(*)(int,int),int b))(int,int)

这能一眼看出是个函数的声明吗?

C家族以外的语言使用一种完全不同的语法风格:

1
2
3
x: int
p: pointer to int
a: array[3] of int

这比较清楚,但是有点啰嗦,省去冒号就是go的风格:

1
2
3
x int
p *int
a [3]int

类似C的main函数,在go中可以这样写:

1
func main(arg int,argv []string) int

我们可以从左到右的读它:

“函数main接收一个int型参数和一个string的slice,并且返回int”
把参数名称丢掉,可以一样:

1
func main(int,[]string) int

现在,用go写上面c的例子:

定义一个函数,它的参数为函数指针:

1
f func(func(int,int),int) int

返回类型还是个函数

1
f func(func(int,int),int) func(int,int) int

不得不说,这确实比着C风格好读许多

这带来了另外一个好处,我们可以定义一个闭包,然后马上调用它:

1
sum := func(a,b int) int {return a + b}(3,4)

这里还隐藏着一个go的思想,区别类型与语句:括号在前,表示类型
括号在后,表示引用,不管是小括号还是中括号
。所以,数组类型的
声明和数组元素的引用就分别是这样:

1
2
var a []int
x = a[1]

函数的连续几个参数是同一类型,前面的类型可省

1
2
3
func add(x,y int) int{
return x + y;
}

多值返回

比如写个交换函数可以这样方便:

1
2
3
func swap(a,b int){
return b,a
}

命名返回值

这个有意思,当返回多个值时,可以明确指出多个返回值到底都表示什么意思,可以当文档使用

1
2
3
4
5
func split(sum int)(x,y int){
x = sum *4 /9
y = sum -x
return
}

变量声明

var关键字

不同于c语言家族,声明变量不能只用类型,而是需要一个显示的var关键字,类型放于后面

1
2
var i,j int
var i,j int = 1,2

我觉得这样带来了一个好处:非常方便的区分了变量定义与类型转换。因为在C中这是不容易
分清的

1
2
int a;
int (a);

省略类型

如果使用表达式初始化变量,类型可省,由表达式推倒而来

1
var i,j = 1,2

省略var,使用:=符号

在函数内可以使用:=符号,省略var 疑问:不能使用在函数外(why)

1
2
3
func main(){
k:= 3;
}

常量定义不用var,而用const

1
const Pi = 3.14

常量不能用:=
疑问:对于惜字如金的go为什么要引入这个符号呢?

go的基本类型

go的基本类型有:

1
2
3
4
5
6
7
8
9
10
11
12
13
bool
string
int int8 int16 int32 int64
unit uint8 uint16 uint32 uint64 uintptr
byte //uint8
rune //int32,表示一个unicode码
float32 float64
complex64 complex128

没有了double,而是给了一个float64,这很友好

只有一种循环结构for

不像C,go的循环条件不需要() 但循环体还是需要{}

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
func main(){
var sum int;
for i := 0;i <= 100;i++{
sum += i
}
fmt.Println(sum);
}

循环条件的前后可省,只剩中间

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
func main(){
var sum,i = 0,0;
for ;i <= 100;{
sum += i
i++
}
fmt.Println(sum);
}

甚至分号都可省,这就跟while效果一样了

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
func main(){
var sum,i = 0,0;
for i <= 100{
sum += i
i++
}
fmt.Println(sum);
}

把所有条件都省略就是死循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
func main(){
var sum,i = 0,0
for {
if i > 100{
break
}
sum += i
i++
}
fmt.Println(sum);
}

这个确实比while(true)还要简洁

if的便捷语句

在if的条件之前执行一个简单语句。这个语句中的变量作用域是if范围内
疑问:这个有啥用呢?

switch

switch不需要每一个case后都加个break

在C语言中经常忘加break,既然经常忘加,就干脆省掉
疑问:确实有需要执行多个case的情况该怎样合适的表达呢?

没有条件的switch

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
func main(){
n := 1
switch{
case n < 0:
fmt.Println("< 0")
case n == 0:
fmt.Println("= 0")
case n > 0:
fmt.Println("> 0")
}
}

这种写法比一长串if else要简洁易读得多

defer

延迟函数的执行知道上层函数返回

这个好,RAII,比C++的构造析构中的奇技淫巧好用的多。应该是从perl中学来的吧
golang的defer有一个特色:defer后面必须是一个函数调用,不能是表达式

没有指针运算

指针加减什么的在go中是不支持的,疑问:为啥不要呢?

结构体类型关键字type

比c语言的teypedefine更加的直观

通过指针间接访问结构体是透明的

还是用.,不是用->

数组类型

数组类型是这样表示的:[n]T

slice类型

slice 可变长序列

1
[]T

这是golang特创的一种类型,在我的认知范围内,其它语言是没有这种类型的。
它实际上提供了一种可变长序列。类似于STL中的vector,perl的array
len(s)返回slice s的长度

构造slice

  1. 初始化一个slice,需要这样

    1
    s := []int{1,2,3,4,5}

    这样是不合法的:

    1
    s := [1,2,3,4,5]
  2. 使用make构建
    构建一个长度为5,每一项都为0的数组

    1
    a := make([]int,5)

    构建一个长度为0,但容量为5的数组

    1
    a := make([]int,0,5)

后面的这种方法在js这类语言中是很方便的,但是golang是不允许的

slice的slice

就是二维数组啦

1
2
3
4
5
6
7
8
9
array := [][]int{
[]int{1,2,3,4,5,6,7},
[]int{1,2,3,4,5,6,7},
[]int{1,2,3,4,5,6,7},
[]int{1,2,3,4,5,6,7},
[]int{1,2,3,4,5,6,7},
}
n := [0][0];

slice的切片

1
array[lo:hi]

它是一个半开区间,数学表示是:[lo,hi)
切片的概念perl也有,但是是用lo和len来表示的,相比perl,golang的更符合人类习惯一些

for循环的range格式

类似于perl中的foreach

1
2
3
4
5
6
7
8
for k,v := range pow{
fmt.Println(k,v);
}
//忽略key,可以这样写
for _,v := range pow{
fmt.Println(v);
}

看来有许多我喜欢的perl的特性哦。这个不知道是不是我自己的感觉,很多语言在设计的
时候都在参考perl–很多人眼中的一门凌乱不堪的语言。es6中抄perl的符号都好几个了,
$ # =>这些

闭包

golang支持闭包,练习题:实现一个fibonacci函数,返回一个闭包可以连续返回的斐波那契数

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
package main
import "fmt"
// fibonacci 函数会返回一个返回 int 的函数。
func fibonacci() func() int {
a := 1
b := 1
index := 0
f := func() int {
defer func(){
index ++
}()
if index > 1{
c := a + b
a = b
b = c
return c
}
return 1
}
return f
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}

对一直习惯了C风格代码的我,温习一下闭包:
闭包就是给函数绑定一些状态(这个函数内声明的变量),并留待下次调用

接口类型

接口类型是由一组方法定义的集合。在C++中是由类的继承,虚函数等来实现的。把它
作为一个类型更自然。java中也有接口(但我不熟悉java,不知道是不是同样的机制)
接口的赋值运算,接收的是一个实现了这个接口的类型(这样达到了泛型的某些效果)
但这个类型是区分指针类型和直接数据类型的,如果指针类型实现了这个方法,那么直接
类型是不能调用的(疑问:这样设计貌似有道理,但2)

隐式接口解耦了实现接口的包和定义接口的包
类型在实现接口的时候不需要显示声明某个方法是个接口。

Stringer接口

fmt.Println调用了被打印对象的Stringer接口,这个接口的定义是这样的:

1
2
3
type Stringer interface{
String() string
}

这就是说,如果我定义一个结构体,如果实现了一个String函数,Println就可以按我
的要求来打印它:
练习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
type IPAddr [4]byte
func (ip IPAddr) String() string{
return fmt.Sprintf("%d.%d.%d.%d",ip[0],ip[1],ip[2],ip[3])
}
func main() {
addrs := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for n, a := range addrs {
fmt.Printf("%v: %v\n", n, a)
}
}

独特的error接口

这个好有特色,把error作为一种接口。

1
2
3
type error interface{
Error() stirng
}

在golang中错误是这样处理的:

  1. 定义一种错误类型
  2. 这个错误类型实现error接口(也就是Error方法)
  3. 用fmt.Println打印它的话就会打印出Error方法返回的字符串
    没见过别的语言有类似的做法,会不会好用要试试看。

Reader接口

Reader接口的Read方法 用数据填充制定字节slice,并且返回填充的字节数和错误信息。
在遇到数据流结尾时,返回io.EOF错误