Go 语言设计得还真敷衍

标签:Go

五年前 Google 推出 Go 语言时,我曾花了几小时的时间把它的语法粗看了一遍,总体感觉是比较乱。
可看到近来 Go 的好评如潮,我还是忍不住又重新学习了一遍。
给我的感觉是几乎每一个语法都没有好好思考,只在某些时候看上去不错,但另一些时候则非常不优雅。

下面就举些例子吧,大部分都记不得了,只能一边重温一边找。

类型放在标识符后面

为了体现与 C 的不同,特意把顺序反过来,看上去很酷,不过不太直观:
var x int = 1
var a, b, c int

而且这种不一致性是为什么:
var (
	a = 1
	b int = 2
	c bool = true
)  // 正常
var a, b int, c bool = 1, 2, true  // 语法错误

函数的返回值位置总觉得有点怪,而且为什么参数列表这样写又支持了:
func add(a, b int, sign bool) int {
	if !sign {
		b = -b
	}
	return a + b
}

再看到方法,知道为什么要这么怪了:
func (x Int) Add(y int) int {
	return int(x) + y
}

返回值还能有多个和命名呢,那么多括号确实摆哪都有道理:
func (x Int) Add(y int) (sum int) {
	sum = int(x) + y
	return
}

首字母大写表示 public,小写表示 private

这条适用于包里的函数、结构体的字段和方法等。

我只呵呵,不评价。

array 和 slice

简单来说,前者是定长的,和 C 语言的数组一样;后者是不定长的,和 Python 的 list 一样。

可这乱七八糟的符号,我半天才发现是不一样的:
var array1 = [5]int{1, 2, 3, 4, 5}
var array2 = [...]int{1, 2, 3}
var slice = []int{1, 2, 3, 4, 5}
这三个玩意的类型分别是 [5]int、[3]int 和 []int。
还有 [][]int、[8][]int、[][8]int 和 [8][8]int 这些变种呢,写个类型真严(蛋)谨(疼)。

map

终于不能像 array 和 slice 一样只用符号了,得引入一个 map 关键字来声明:
var m map[string]int
要是 value 比较复杂,会不会很头晕:
var m map[string]map[string][]string

类型转换

bool 和整型之间不能进行类型转换:
var a = int(true)
var b = bool(1)
// 以上都会出错

array 和 slice 转换时要注意元素类型:
var array = [...]int{1, 2, 3}
var slice1 []int
slice1 = array[:]
var slice2 []int32
slice2 = array[:] // 报错
var slice3 []int64
slice3 = array[:] // 还是报错
就是这么严谨。

自定义类型和原始类型不能混用:
type Int int

func (self Int) Add (another int) int {
	// return self + another
	// return self + Int(another)
	// 以上都是错的,以下才是对的
	// return int(self + Int(another))
	return int(self) + another
}

interface

Go 的接口和很多语言都不一样,一个类型可能是被动实现接口的。
比如上面我给 Int 实现了一个 Add 方法,如果我再加一个接口的定义:
type Number interface {
    Add(int) int
}
Int 就莫名其妙地实现了 Number 接口了,我在任何声明需要用 Number 的地方,都可以用 Int 类型的对象了。
可一旦未来我的 Number 接口需要变化,比如增加一个 Sub 方法,所有的调用者和实现者好像就傻眼了……

而且这个接口用来实现泛型有点尴尬,比如 Go 的排序甚至比 C++ 和 Java 还麻烦。
具体实现可以看 sort 包里的例子。
需要对排序的 slice(例如 []int)定义一个类型(例如 T),再给这个类型实现 3 个方法,然后将这个 slice 转换成 T 类型,最后传给 sort.Sort 方法。
当然,其中一方面是因为没法直接给 []int 定义方法,另一方面则是这里本该用传递函数来实现。

另外,接口也能进行类型转换:
type A interface {}
type B interface {
	Test()
}

var a A
var b = B(a)  // 编译期错误
var b = a.(B)  // 运行时错误
又引入一个蛋疼的语法,为什么 a.B 和 a.(B) 不是一回事?

此外,Go 里面可以用 interface{} 这种空接口来替代任意类型,有点 void* 的意味。可是需要传递 []interface{} 的地方,却不能传递 []int 这种具体的类型。
例如:
package main

import "fmt"

func first(slice []interface{}) interface{} {
	return slice[0]
}

func main() {
	s := []int{1, 2, 3} // 报错
	f := first(s)
	fmt.Println(f)
}
一种办法是修改声明:
s := []interface{}{1, 2, 3}
另一种办法就是转换每一个元素:
func main() {
	s1 := []int{1, 2, 3}
	s2 := make([]interface{}, 3)
	for i, v := range s1 {
		s2[i] = v  // 这里可以不用写成 interface{}(v)
	}
	f := first(s2)
	fmt.Println(f)
}
优雅得无法直视。

差点忘了 Go 还支持可变参数,例如 fmt.Sprintf 的函数签名是这个:
func Sprintf(format string, a ...interface{}) string
那个 a ... 就是可变参数了,本身是一个 slice,所以在上文中应该是 []interface{} 类型。

于是来实现一个输出 IP 地址的方法:
type IPAddr [4]byte

func (ip IPAddr) String() string {
    return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}

这样当然不优雅,如果是用 Python,直接 *ip 就行了,Go 里面本来也应该这样:
func (ip IPAddr) String() string {
	return fmt.Sprintf("%d.%d.%d.%d", ip ...)
}
但是类型不符,必须这样写:
func (ip IPAddr) String() string {
	addr := make([]interface {}, 4)
	for i, v := range ip {
		addr[i] = v
	}
	return fmt.Sprintf("%d.%d.%d.%d", addr ...)
}
我当时那心情,简直就和元首一样。

指针

Go 的指针和 C 的指针比较像,* 和 & 的用法基本一致。
一个奇怪的不一致处是 (*x).y,当 y 是 *x 的一个属性时,可以简写为 x.y(C 是用 x->y);而当 y 是 *x 的一个方法时,就不能这样缩写了。
原因是 T 和 *T 是不同的类型,给类型 T 定义的方法,是不能用在 *T 的变量上的;而 *T 的方法也不能用在 T 的变量上。

channel

Go 使用 channel 来在 goroutines 之间通信,并在阻塞时切换 goroutine。

虽然类型声明难看点,不过 <- 看上去还算直观
var ch chan int = make(chan int, 10)
ch <- 1
ch <- 2
ch <- 3
ch <- 4
可是接下来是什么鬼:
<-ch
x := <-ch
y, z := <-ch, <-ch

其他诸如错误处理之类的我就不说了。总体而言,Go 的语法给我的感觉是设计初期考虑不周,后期胡乱地缝补,导致一致性不强。
至于优势,据说只有部署方便(仅依赖 glibc)和并发编程这两点。

5条评论 你不来一发么↓ 顺序排列 倒序排列

    向下滚动可载入更多评论,或者点这里禁止自动加载

    想说点什么呢?