published on
tags: tech Go

Go语言观察笔记 [上]

正文之前

最近吉他班上完了,在凑齐人上高级班之前多写点Post。

本来想写点Continuation相关的东西,类似call/cc, coroutinecps什么的, 但是暂时没拿准怎么动笔比较好。这些东西在我自己搞懂之前,看过不少讲解,还有很多例子和比喻,最后都不知所云。搞懂了之后再去看,就知道在讲什么了。所以我可能会试着写一点draft,但是要到能放出来可能还要花一些时间去琢磨下。

最近对Go有了点兴趣,想看一看。当然很大程度上我只是想找一门带有以下功能的语言:

  • 静态类型
  • GC
  • 丰富的库,易用的包管理器

语言设计本身倒是题外话了,比如我最喜欢scheme,但是其标准库实在是没什么到操作系统的接口,能做的实事就偏向纯计算了。什么你说有扩展,好吧每个实现扩展的都不一样…… 这用起来也太残念了吧。

所以现在先来看Go,看完了准备看Rust。

于是这里的笔记和评论是来自一个初学者的看法,所以大家不要吹毛求疵了。

路径

Go的开发涉及两个路径,一个叫GOROOT,一个是GOPATH。简单来说GOROOT是Go安装的位置,GOPATH是开发的工作区。

如果像我这种懒人在Windows下用msi安装的,只要配置一个GOPATH的环境变量即可。Go相关的程序在GOROOT/bin下,而第三方程序和你自己编译出来的就在GOPATH/bin下

另为了配合包管理器,最好顺便把git和hg装上

Tutor

Go很不错的一点是自带了一个指南,我就准备一边看指南一边往下写了。

首先指南被分为3部分,基础概念,方法和接口,以及并发。那么我预计也拆3篇分别写写。

基础概念

分包是个好的idea, 新一点的语言大多都带上了这个特性。

Go里面需要显示在文件头部写上package xxx,如果是lib,就取最近一级文件夹名(其实应该是建议文件夹名就取lib名),如果是执行程序,就用package main

如果是导入的话写成

import "fmt"
import "math"

import (
    "fmt"
    "math"
)

当然按照我的习惯,肯定会选前一种,因为每一行做一件事修改的时候看diff会比较舒服类似:

 - import "math"
 + import "net"

 - "math"
 + "net"

这样你会发现看下面这个光看修改的这一行不会很确定是改了啥,这就需要diff提供足够多的上下文了。这其实也是一个你最好每行只定义一个变量的好理由。

函数和类型定义

Go的类型表达式其实很搞,从看上去的样子推测。应该是故意采用把c语言的定义方式反过来似的。

比如(先抛开var func这两个关键字) :

  • C:int a,Go:a int
  • C:int a[3], Go: a [3]int
  • C:int *a, Go: a *int

然后函数定义:

  • C: int a(int x, int y), Go: func a(x int, y int) int

怎么样,像不像是故意反过来的。

Go的网站上其实有一个专门的页面讨论了这个问题,因为这种类型的写法人比C的容易理解,更加nature。

嗯,当然了,这种后置的类型写法自然比C容易理解。当然说实话我觉得能比C的类型写法更难理解的语言实在是不多了。你既然都不沿用C的写法了,就不好好考虑下把类型的声明方式搞好一点么。

这样一后置,哪怕学学pascal,写成func a(x:int, y:int):int我觉得也看得爽一点。

来换用不同的语言上几个例子感觉一下:

  • Go: func map(f func(int) int, s []int)
  • C: int[] map(bool (f *)(int), int s[]),当然C其实没有bool,数组还要另外传长度,这样太苦了我们就放过把。
  • C++: vector<int> map(function<int(int)> f, vector<int> s),当然为了写短点我把名字空间去掉了。
  • Pascal: function map(f : Functor, s : IntArray) : IntArray,当然pascal很贱的不让你写一个复杂的类型表达式,而只能一步一步通过中间的类型名称来。所以pascal种类型名字很重要。
  • Haskell: map:: (Int -> Int) -> [Int] -> [Int]

懒得列下去,欢迎大家多找找。

Go这里还有另外一个比较蛋疼的设计就是可以缩写:比如func go(x int, y int) int可以写成func go(x, y int),这个实在是太囧了,我看了好久都不能习惯。原因在于你会注意到这里有一个微妙的优先级不一致性:在前一个写法中,我们看到的东西类似于[x int], [y int],而在第二个场景下就变成了[x,y] int,注意按照这个标注,在前一个方案里面,空格的优先级是比较高的,而在后一个方案里面,又要让x y先按,结合起来,再去一起看后面的类型。这应该就是这里有点反直觉的感觉的地方吧。

由上我不推荐使用这个缩写,其实Go为了简化写法带来的设计基本都有点不给力,实在是郁闷。

然后函数可以返回多个值,如例

func swap(x, y string) (string, string) {
    return y, x
}

不得不说要能写成

func swap(x:string, y:string) : (string, string) {
    return y, x
}

不是很好嘛。不过这个功能是很好的,而且还有可以写成命名返回的方式。类似Pascal中通过给函数名赋值来指定返回值:

func swap(x string, y string) (a string, b string) {
    a, b = y, x
    return
}

变量定义

写法:

   var x, y, z int
   var x, y, z int = 1, 2, 3
   var b1, b2, s1 = true, false, "no"

上面第三行提供了类型推倒

另一个带了类型推导的写法:

b1, b2, s1 := true, false, "no"

为什么要发明一个这么不一致的写法,我只能猜测,大概是为了写出下面的代码:

for i:=0; i<10; i++ {
     fmt.Println(i)
}

好吧,我也不知道在Go里面为什么不支持写成: for var i=0; i < 10; i++ {,而非要发明新的写法。等我再看到后面如果发现别的地方要用到再说好了。

另补充,把var换成const可定义常量

基本类型

  • bool
  • string
  • int int8 int16 int32 int64
  • uint uint8 uint16 uint32 uint64 uintptr
  • byte
  • rune
  • float32 float64
  • complex64 complex128

控制流

  • 循环: for
  • 条件: if,条件中可以加分号;,这样可以像for一样先执行一句话,应该是为了方便定义一部分只在if中用的变量(同时在条件和body会用的那些),这样可以限制这些变量的作用域,使变量局部性更好,代码更容易读(过了这段就可以忘了这个变量了)。

Struct

基础篇里面的struct估计是在为后面的接口篇打基础吧,就当是这样好了,先接着看。

定义一个struct,老规矩,和C反过来:

type Vertex struct {
    X int
    Y int
}

恩,这里也可以缩写,缩写也是和c的缩写反过来的X,Y int

使用和C一样都是用.,初始化的可以直接指定每个字段的值,否则就默认填0了

指针: 类型如上所述是写成 *Vertex,Go里面指针直接用.就相当于->了。指针不能进行算数运算(否则没办法实现靠谱的gc)

通过new(Vertex)可以分配一个新的Vertex,虽然Go里面把new称作函数,但是估计还是当作一个运算比较好,因为其接受的是一个类型而不是值。到目前为止暂时没看到Go提到类型是值的内容,因为听说有反射,估计可能确实类型就是值,等看到了就当这段没说好了。

Slice

Slice就是带有长度的数组,类似C++的vector,但用处不同,Go tutor链接到了一篇文章专门讲Slice的,留待后面看了补一篇关于Slice的讨论好了

For-Range

for还有另外一种写法:

for i, v := range []int{2, 3, 5, 7} {
    fmt.Printf("%d: %d\n", i, v)
}

map

map就是来自C++的map,存放一个字典。写法略微妙:var m map[string]Vertex,表示keystring类型,valueVertex类型。嗯,再揣摩一下设计的用意是说这个m的类型表示你需要用一个string类型的值放到方括号内,像这样:m[str],你就得到了一个Vertex类型的值嗯。好吧管他原意是什么,就这样吧。

字面量的写法类似Json。

delete(m, key) 用来删除。 elem, exist = m[key]。这样可以用exist检查是否在其中。

函数可以作为值使用

也支持词法闭包,恩非常好

switch

switch语句不需要手工写break了,如果要往下,就写上fallthrough。可以去掉表达式当作lisp中的cond语句来用

上篇完

还剩下slice,之后来讨论。

接下去应该就要进入接口篇了。