Go是一门比较新的语言,是由一群牛人在Google推出的,这些牛人大多来自贝尔实验室的Plan 9操作系统项目和Limbo语言设计项目,因此大多认为Go的前身就是Limbo。同时这些牛人中有C语言的设计者Ken Thompson,因此Go的设计里面也包含了一些C的思想或者去解决一些C语言上效率的问题。

它的主要语言特性有:

  • 自动的垃圾回收,针对内存泄漏和野指针
  • 原生支持一些复杂的类型,字典、数组切片(动态数组)
  • 匿名函数和闭包
  • 有限的类型和借口
  • 多函数返回值
  • 统一的错误处理机制,defer、panic、recove
  • 原生支持并发编程
  • 反射
  • 语言交互性好

果然是一门“现代”语言,一门“更强的C语言”。

这个系列的几篇博客,主要就是记录下Go的基础要点,以及一些学习过程中的思考,也希望对有缘的人有所帮助。

  • 基础,就是和其他语言有什么不同,主要是C和C++
  • 面向对象,简单必要的面向对象设计
  • 并行编程,这是Go语言的优势所在
  • 高级特性,一些特性或者装逼情况下可用

本文中的内容绝大多数都参考了大牛许式伟的《Go语言编程》,所以也相当于是学习笔记了。

1. Go程序的基本结构

开始一门语言的学习,基本上先要了解这门语言的程序是怎样组织起来的,代码的基础组成是怎样的,怎样能让一个程序运行起来。Go在程序结构上就体现出了它的先进性。 如下就是一个非常简单的Go程序:

package main

import "fmt"
import "os"
import "strings"

func f(x int) int {
    if x == 0 {
        x = 5
    } else {
        x = 7
    }

    return x
}

func main() {
    fmt.Println(os.Args[0])

    for index, arg := range os.Args[1:] {
        fmt.Println(index, ":", arg)
    }

    fmt.Println(strings.Join(os.Args[1:], " "))

    fmt.Println(f(3))
    fmt.Println(f(5))
}
  • package,指定源文件的包名,包名可以理解成一个针对文件的namespace,一个对应于python中文件名的功能,总之用来表征一个文件中符号的所属
  • import,需要引入其他包的变量或者函数
  • func,定义函数,这里和其他一些常见函数最大的区别就是,返回值在最后面;同时”{“符号必须和func同行
  • if,条件判断语句,比较大的区别就是判断条件没有用”(“包围起来,同func,“{”必须与if同行
  • for, 循环语句,在这个例子中直接调用range遍历一个数组,并将相关变量赋值,也有我们常见的那种形式,后续可能会提到
  • fmt,调用Println打印信息,每个函数总有相应的printf

除了上面的一些关键字外,还有个特点,支持多重赋值,for循环的index,arg两变量就是在一条语句中赋值的,这个特点也非常使用。

实现很多很多这种格式的代码文件,然后通过import关联起来,这样就组成了我们的Go项目了。

<calcproj> ├─<src>
              ├─<calc>
                ├─calc.go
              ├─<simplemath>
                ├─add.go
                ├─add_test.go
                ├─sqrt.go
                ├─sqrt_test.go
            |─<bin>

将calcproj目录加到环境变量GOPATH中,export GOPATH=$GOPATH:xxx:xxx:calcproj,然后运行go build calcproj即可获得相关的执行文件了。

编译是不是也非常简单?工程效率太高了,不用管理复杂的makefile了。

2. 变量

  • 和前面提到的函数一样,Go变量最大的不同就是变量类型是在末尾:var i int
  • 函数初始化,可以有很多方法:
    • var v1 int = 1
    • var v2 = 3
    • v3 := 5

    第三种方法最简单,这个该说是真现代,还是真懒呢?但是用多了,可能就会潜移默化吧。第二种和第三种编译器都会去根据赋的值推到出变量类型。但需要铭记的是Go是一个强类型的静态语言,即使这个特定和一些动态语言类型,它也是编译器静态编译出确切的类型的。

  • 前面也提到,Go支持多种赋值,比如i, j = j, i,通过多重赋值非常简单的实现了两个变量交换的功能。
  • 后面会介绍,函数返回值支持多变量返回。同时变量支持匿名变量,这样对于函数返回值中不感兴趣的可以使用匿名变量”_“,_, a, _ = f()假设f返回3个值,a获得第二个值
  • 有个特殊的常量iota,在每一个const出现时,置为0,在下一次const出现前,每出现依次iota,则加1。另外如果前后const表达式是一致的,可以省略后面的iota。通过这个功能可以很容易实现枚举。
const (
    c0 = iota     // c0 = 0
    c1 = iota     // c1 = 1
    c2 = iota     // c2 = 2
)

const (
    c3 = 1 << iota     // c3 = 1
    c4 = 1 << iota     // c4 = 2
)

const (
    c5 = 1 << iota     // c5 = 1
    c6            // c6 = 2
    c7            // c7 = 4
)

在一些特殊场景下,用起来还是非常方便。

3. 类型

除了常见类型,支持一些常见的其他语言需要用库来实现的类型。

3.1 内置类型

除了C中支持的类型外,还额外支持如下类型,或者和C有一些小的区别:

  • 布尔类型,Go语言的bool类型比较特殊的地方在于,bool类型不支持其他类型赋值,不支持自动或者强制类型转换,也就是说bool类型就是ture或false
  • 复数,支持常见复数表示初始化,或者通过complex函数,并且可以通过real函数获得实部,imag函数获得虚部
    value1 := 3.2 + 12i
    value2 := complex(3.2, 12)
    
  • rune,原生支持的Unicode类型
  • 数组,Go中的数组和C的意义基本一致,但
    • 定义格式有差异,类型放在括号后,比如[100] *int,这表示申明了一个int型的指针数组
    • 数组是一个数值类型,而非C语言中的指针类型,这意味着什么呢?将一个数组赋值给另一个同格式的数组变量,以及将数组传递给函数,它都是按照值来拷贝的,修改后者不会影响前者的值,非常大的不同。而如果就是想修改原数组则需要和其他类型一样,使用对应的指针类型,比如
      a := [10]int{1, 2, 3, 4, 5}
      c := &a
      c[3] = 1
      

      这样a就被修改了。

    • 除了支持C语言中的按照下标来访问,还可以使用range,按照迭代器的模式遍历数组
  • 数组切片,类似于动态数组,C++中的std::vector等功能,
    • 定义的时候就是普通数组中不要指定数组元素的个数即可。也可以通过make函数来创建v := make([]int),效果等同。
    • 添加元素使用append函数
    • 通过cap函数能返回已分配的空间大小,len函数则返回当前元素个数
    • copy函数能将一个数组切片的数据赋值给另外一个数组切片,自动处理两个切片大小的问题
  • 字典map,Go在语言层面直接支持字典,类似于C++的std::map
    • 字典声明的形式如下:var v map[string] int,string是key的类型,int是value的类型
    • 创建字典的形式如下:v = make(map[string] int)
    • 元素赋值的形式如下:v["beijing"] = 0
    • 删除元素的函数如下:delete(v, "beijing")
    • 查找元素的方法如下:value, ok = v["beijing"],ok是一个bool值,通过它判断是否查找到元素

是不是非常使用,增加的类型基本上都是日常工作中经常要使用的,在一些其他语言中,往往需要以来库来实现,增加了理解和调试的难度,同时Go�又没有随便增加一些从语言层面很难理解的类型,或者小众类型。

所以,我还是认为Go就是一门充分考虑工程的语言,而不是一些金玉其外的语言。

3.2 类型系统

类型系统,是指一个语言的类型体系结构。一个典型的类型体系结构需要考虑如下内容(在这里需要说明下,下面内容是大牛许式伟Go语言编程中专门提及的):

  • 基础类型,就是那些常见的int、float、byte的类型,是其他类型的基础
  • 复合类型,由基础类型组合而成的类型,数组、结构体、指针等
  • 可以指向任何对象的类型(Any类型)
  • 值语义,引用语义
  • 面向对象,即具备面向对象特征的类型
  • 接口

3.2.1 为类型添加方法

Go语言中一个非常特殊的地方在于,任何类型,都可以直接添加额外的方法,而不用继承或者其他高级语言的一些特性来实现。

比如给常见的int类型增加一个比较的方法,可以用如下方法实现:

type Integer int
func (a Integer)Less(b Integer) bool {
  return a < b
}

var a Integer = 3
if a.Less(2) {
  fmt.Println("a is less than 2")
} else {
  fmt.Println("a is more than 2")
}

非常非常简单!

3.2.2 值语义与引用语义

Go语言中只有如下四种类型看起来像引用类型,其他类型都是纯粹的值类型,包括数组:

  • 数组切片,指向数组的一个区间
  • map,字典,按照key来查询
  • channel,goroutine间的通信设施
  • interface,对一组类型的抽象

3.2.3 结构体

Go语言的结构仅仅具有组合的功能,不同于C++中那么丰富的重载、继承等丰富。另外结构体函数的支持,也如前面3.2.1介绍的和其他类型一样支持。

需要注意下结构体初始化的方式有一下几种:

rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{0, 0, 100, 200}
rect4 := &Rect{width: 100, height: 200}

3.2.4 匿名组合

确切的说,Go语言也提供了“继承”的功能,但是采用组合的文法。

这种类型提供了一些比较灵活的对象处理方式,计划后续专门写文章探讨这个类型。

4. 流程控制

除了类型,计算机语言第二个比较基础的部分就是流程控制。Go语言提供了和C语言类似的流程控制语义。有些流程控制在前面的例子也有所提及。

  • 条件语义,if、else、else if,没有什么写新奇的,不一样的就是前面提到的,判断语句不用被括号包围,代码段的花括号与关键字同行
  • 循环语义,for,没有while的,和C语言不一样的,初始化、判断、置后操作都不需要被括号扩起来,代码段的花括号与关键字同行;另外可以使用迭代器形式的range
  • 选择语义,switch,不一样的地方在于每个case都某人执行本case代码段,不提供break关键字,如果需要同时执行后续case代码段,需要使用fallthrough语句
  • 跳转语义,goto,特殊地方用到

5. 函数

函数在前面例子中已经多次提到,这里仅仅说一些和C语言中函数不太一样的地方。

5.1 不定参数

Go语言在函数中传递不定参数非常容易,仅需要在函数定义时使用...type“类型”接口,例如:

func myfunc(args ...int) {
  for _, arg := range args {
        fmt.Println(arg)
    }
}

上面这段简单的代码就实现一个可以传入不定参数的函数,每个参数类型是int。

那么在其他函数中就可以通过如下两种形式进行访问:

func myfunc3(args ...int) {
        // 使用不定个数的int参数进行调用
        myfunc(2, 3, 4)

        // 按照原样传递参数
        myfunc(args...)

        // 传递不定参数的数组切片
        myfunc(args[1:]...)
}

如果传入的不定参数类型不一样,只需要在定义的时候使用interface{}即可。interface是一个特殊的接口,类似于java中的java.lang.Obeject,可以引用任意对象。具体内容在后续面向对象的内容中会介绍。

func Printf(format string, args ...interface{}) {
  // ...
}

5.2 多返回值

前面介绍过,函数可以返回很多值,同时通过匿名变量获得自己感兴趣的返回值。非常简单,不多废话。

5.3 匿名函数

就是没有名字的函数,可以直接赋值给一个变量,后续就用变量那调用该函数,也可以作为代码块直接执行。

匿名函数主要就是为了实现“闭包”的概念。闭包可以引用到函数外的变量,只要闭包还被使用,相关变量就会一直存在。

func main() {
  var j int = 5
  a := func()(func()) {
      var i int = 10

      return func() {
        fmt.Printf("i, j: %d, %d\n", i, j)
      }
    }()

  a()

  j *= 2

  a()
}

上述代码中将一个匿名函数赋给a,func()(func()),即没有输入参数,返回参数是另外一匿名函数,主要功能就是打印变量。

它和直接在函数中打印对应的变量有什么区别呢?如果要在这段代码中匿名函数外修改i的值是不可能的,因为它被匿名函数包围起来,只能在匿名函数内部使用,反而j可以被匿名函数访问。也就是说匿名函数对i起到了保护作用,就是将i“包”了起来。

同时定义的匿名函数最后有对括号,它可以用来传递输入参数,同时执行匿名函数。上面代码对意思也就是说,定义了一个没有输入参数,返回值是匿名函数的匿名函数,同时直接运行,将返回的匿名函数赋值给了a。比较绕,但多想想就明白了。

其他Go语言入门文章:

Go语言快速入门(二):面向对象

Go语言快速入门(三):并发编程