Go

Go 语言中的 Slice

published on

按之前的笔记中许诺的,这回专门来讨论一下Go语言中的Slice。

构造

首先,我们可以看看这个东西是怎么实现的: 用C++来表示,它应该就是这样一个struct:

template <typename T>
struct Slice {
    T* buf;
    size_t size;
    size_t capacity; 
}

其中buf指向了一个数组,size是当前Slice的大小,capacity是指向的数组有多长。

相应的就有两个重要的函数lencap,分别返回sizecapacity

如果你了解C++的标准库,会不会觉得这个结构很眼熟?对了,C++中的vector就是这么实现的。当然在Go中,得益于gc机制,Slice就可以放开手做更多的事情了。

首先看Slice的构造, 两种方式:

  1. 为buf新申请一段空间: make([]T, length) or make([]T, length, capacity)
  2. buf指向已有的数组: a[low:high]

vector不同的是,可以Slice指向已有的数组,而vector不能。

代替vector

我们先看最基础的功能,使用Slice来代替C++中的vector。

此时所需要的两个最核心的功能就是push_backpop_back,使用Slice的时候可以使用这两个函数的强化版本:

push_back对应到append,在Go里面可以使用append添加一个或者多个元素到Slice的结尾。由于从文档中看不出来会不会使用capacity*2的策略,所以如有需要可以自己写一个push_back如果需要扩展capacity就乘2好了。

pop_back就很简单了,直接用Slices功能,从现有的里面切出来一段新的即可。

由于上面说到自己实现append,那么就需要一个copy函数,把旧的Slice中的内容复制到新的中,这个函数语言中也已经提供了~

当然上面说了这么多,因为对于这些函数来说,可能会通过指向一个新的Slice来完成扩展的操作,所以不会和vector的接口做的完全一样。不过为了使用方便,我们当然可以把它做得完全一样,只要自己写一个struct将Slice包一下就好了。

表示已有数组区间

有时候,我们并不是需要动态改变Slice的大小,只不过是想要表示一个现有数组的一段。你有没有在用C/C++时在程序中出现过类似下面的函数签名:

void func(T* arr, int left, int right)
void func(T* arr, int size)
void func(T* begin, T* end)

现在好了,只要一个Slice,里面这些信息都包含了。看到那里面的beginend会不会让你想到很多的函数呢,是的,现在他们可以只需要一个参数了:)

少量用到数组区间的算法的例子:

  • 快速排序
  • 线段树

小结

这里简单的讨论了go语言中Slice的内部结构和一些使用场景。许多别的语言中要实现一个类似的玩意也不难,在没有gc的语言中如果要使用需要留意,用起来可以带来一些便利性。

Read More...

Go语言观察笔记 [中]

published on

方法的定义

Go中可以给Struct定义方法(只要是同一包中,非结构体也可),类似给一个类添加成员。只是这个方法定义的时候不写在Struct中,只要是同一个包中的文件都可以对它扩展,为它添加方法。这个设计还是很好的。有点类似C++中的namespaceclass,你可以在class中定义public static的成员来实现和namespace同样的效果,但是namespace可以把这些内容拆到不同的地方,而不用挤在一起。

写法:

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

然后有一个新的类型:接口

type Abser interface {
    Abs() float64
}

接口类型的变量可以保存任何一个实现了其中对应函数的类型的值,有点类似duck type的感觉。这带有一点在显示的实现声明和纯动态类型中间的折衷把,属于用不那么严格的约束来换取便利。

这个玩意如何比较好的实现是个有意思的事情。初步估计应该是类似一个vtable那个的结构,每个值都是一个闭包。当然因为这里的闭包的环境实际上都是共享的,所以存一个指针就好了,恩这样创建一个新的时候也只要设这一个指针,别的设置在编译期都可以搞定。

这件事情我之前在玩lua的时候倒是干过。我用C++的模版元编程机制实现了一个把C++的类自动导入到lua中的办法,在lua里面就是用table + closure来实现的。方便我可以自己写成:

a.do_sth();
-- but not  a:do_sth()

因为luaa:do_sth这个设计实在是不爽,你就算不小心写错成.了,语法上也没任何问题,运行时搞不好都检查不出来,行为是不确定的。主要的缺点就是如果table中方法太多了,创建起来比较耗时,所以我一般就导导函数给lua,而不用什么类。

再回来看接口,tutor介绍了一个常用的叫error:

type error interface {
    Error() string
}

Tutor中这一篇到这居然就完了,往后走就是并发……

好吧,你的类型系统里是不是少了点什么? datatype呢…… 什么原来没写在tutor里,我在effective_go里找到类似的东西了:type switch

恩这个卧槽的做法如下:

func f(x int) interface{} {
    if x < 0 {
        return 3
    } else {
        return "str"
    }
}

func main() {
    var t = f(2)
    switch t.(type) {
    case string:
        fmt.Println("string")
    case int:
        fmt.Println("int")
    }
}

说实话有了这些基础的话,用起来就没什么问题了,并发这件事情并不是干什么都要用的,实际上Go的并发模型基本上是为服务器设计的,所以之后再慢慢看。

Read More...

Go语言观察笔记 [上]

published on

正文之前

最近吉他班上完了,在凑齐人上高级班之前多写点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,之后来讨论。

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

Read More...