缘起

转 Go 语言开发有一段时间了,一直想输出一些 Go 语言学习相关内容,算是沉淀沉淀自己,又或者说给后来者一些避免踩坑的指导。但是又不想零零碎碎的总结或者误人子弟,所以,拖了很久,现在决定系统地从零开始输出一些系统性,亦或者是一些源码解读的内容。那么,现在就开始吧!

简介

Go 语言是谷歌开源的一门编译型语言,集简洁、快速、安全、高效、并行于一体的一门天生并发的语言。

语法基础

一门编程语言最基础的无非就是基本数据类型以及数据结构和流程控制,所以,我们不必有什么学习难度上的包袱,跟着我往下看,抛开包袱,轻松地从最基本的语法开始学习 Go 语言。

变量

Go 语言引入了关键字 var 对变量进行声明,也可以使用 := 来对变量直接进行初始化,Go 编译器会自动推导出该变量的类型,这大大的方便了开发者的工作。但是需要注意的是 := 左侧的变量不能是已经被声明过的,否则会导致编译器错误。 同时,Go 语言变量如果没有进行初始化, 在编译期间,编译器会对该变量以默认值进行初始化。 举个例子看看 Go 语言变量如何进行声明:

// var 声明
var a int
a = 12
// := 直接初始化
b := 12
b, c := 13,"hello" // 左表达式必须要有未声明的变量才能用:=,如果全部声明过了直接用=

常量

常量顾名思义,一直不变的变量。Go 的常量定义可以限定常量类型,但不是必需的。如果定义常量没有指定类型,那么该常量就是无类型常量。当然在编译阶段,Go 的编译器会根据字面值推断出常量的类型。

const TYPE = 5
const PRODUCT uint16 = 1421
const Pi float64 = 3.1415926
const x,y int = 1,2 //多重赋值

对于常量,还需要掌握的一个知识点就是:iota,iota 是 Go 语言的一个关键字,一般和 const 配合使用,在 const 关键字出现时会被重置为 0 。const 每增加一行变量,将使得声明增加1。 看下面的例子,就能很清楚知道 iota 的使用了。

const (
    a = iota // 0
    b       // 1
    c = "hello" // "hello"
    d       // "hello"
    e       // "hello"
    f = iota // 2,这里会接着上面中断的地方继续计数,这就是iota神奇的地方
    g       // 3
    h       // 4
    )

数据类型

一门编程语言的基石就是数据类型,通过不同的数据类型我们能存储并处理不同的数据格式的数据。Go语言的数据类型主要有如下类型: * 整型

|类型|说明| |---|---| |byte|uint8| |int|根据不同操作系统平台,也许是 int32,也许是 int64| |int8|[-128,127]| |int16|[-32768,32767]| |int32|[-2147483648, 2147483647](10位)| |int64|[-9223372036854775808, 9223372036854775807](20位)| |rune|等同于 int32| |uint|根据不同操作系统平台,也许是 uint32,也许是 uint64| |uint8|[0,255]| |uint16|[0,65535]| |uint32|[0,4294967295]| |uint64|[0,18446744073709551615]| |uintptr|一个可以恰好容纳指针值的无符号整型(对 32 位平台是 uint32, 对 64 位平台是 uint64) |

在 Goland 中可以通过unsafe.Sizeof获取某类型长度,类似 C 语言的sizeof()

浮点数和复数

|类型|说明| |---|---| |float32|±3.402 823 466 385 288 598 117 041 834 845 169 254 40x1038 计算精度大概是小数点后 7 个十进制数| |float64|±1.797 693 134 862 315 708 145 274 237 317 043 567 981x1038 计算精度大概是小数点后 15 个十进制数| |complex32|复数,实部和虚部都是 float32| |complex64|复数,实部和虚部都是 float64|

bool 类型

Go 语言提供了内置的布尔值 true 和 false。Go 语言支持标准的逻辑和比较操作,这些操作的结果都是布尔值。值得注意的地方是可以通过 !b 的方式反转变量 b 的真假。需要注意的是布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。

字符串

Go 语言中的字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。UTF-8 是被广泛使用的编码格式,是文本文件的标准编码,其它包括 XML 和 JSON 在内,也都使用该编码。由于该编码对占用字节长度的不定性,Go 中的字符串也可能根据需要占用 1 至 4 个字节,这与其它语言如 C++、Java 或者 Python 不同。Go 这样做的好处是不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。

Go 语言中字符串的可以使用双引号 (") 或者反引号 (`) 来创建。双引号用来创建可解析的字符串字面量,所谓可解析的是指字符串中的一些符号可以被格式化为其他内容,如 \n 在在输出时候会被格式化成换行符,如果需要按照原始字符输出必须进行转义。而反引号创建的字符串原始是什么样,那输出还是什么,不需要进行任何转义。

在 Go 语言中单个字符可以使用单引号来创建。之前的课程中,我们有学习过 rune 类型,它等同于 int32,在 Go 语言中,一个单一的字符可以用一个的 rune 来表示。这也是容易理解的,因为 Go 语言的字符串是 UTF-8 编码,其底层使用 4 个字节表示,也就是 32 bit。

在 Go 语言中,字符串支持切片操作,但是需要注意的是如果字符串都是由 ASCII 字符组成,那可以随便使用切片进行操作,但是如果字符串中包含其他非 ASCII 字符,直接使用切片获取想要的单个字符时需要十分小心,因为对字符串直接使用切片时是通过字节进行索引的,但是非 ASCII 字符在内存中可能不是由一个字节组成。如果想对字符串中字符依次访问,可以使用 range 操作符。另外获取字符串的长度可能有两种含义,一种是指获取字符串的字节长度,一种是指获取字符串的字符数量。

格式化字符串

Go 语言提供标准库 fmt 包来提供格式化功能。具体可参看官方文档

字符类型

在 Go 语言中支持两个字符类型,一个是 Byte(实际上是 Unit8 的别名),代表 UTF-8 字符串的单个字节的值;另一个是 rune,代表单个 Unicode 字符。

Go 语言的多数 API 都假设字符串为 UTF-8 编码。但是 Unicode 字符在标准库中有支持,只是实际很少使用。

数组和切片

Go 的数组就是一种固定长度和固定对象类型组成的数据类型,数组是值类型,即当数组变量被赋值时,会获得原数组的拷贝。新数组中元素的改变不会影响原数组中元素的值。

var a [4]int
var b [...]int{1,1}
a1 := [...]int{1,2,3,4}
b1 := a 
b1[2] = 123 // 因为 Go 是值传递,a1,b1 有各自的地址空间,所以b1的改变不会影响原数组

Go 切片其实是一个动态数组,我们在使用数组的时候通常会并不知道我们需要多大的数组,这时候就需要动态数组(切片),切片并不存储任何元素而只是对现有数组的引用。切片本身不包含任何数据,它仅仅是底层数组的一个上层表示。对切片进行的任何修饰都将反映在底层数组上。切片的具体实现原理,我们之后会进行剖析,现在先学会使用就可以了。

切片的长度是指切片中元素的个数。切片的容量是指从切片的起始元素开始到其底层数组中的最后一个元素的个数。 关于切片的更多知识,我们之后再分析。我们只需要知道切片是原始数组的引用,和数组一样来使用就可以了,不要有过多的思想障碍。

map

map 即key,value 键值对,其底层实现是一个hashmap,以后我们会进行深入详解,这里我们先知道怎么使用就可以了。

m := make(map[string]string) // 声明
    m["hello"] = "world"
    m["key"] = "value"
    for k, v := range m { //遍历map
        fmt.Println(k,v)
    }

chan

chan 通道,可以理解为就是一个队列容器,头进尾出。用于存取数据。一般用来 Goroutine 之间的数据交换以及 Goroutine 协作控制。

ch := make(chan int,2) // 能存放两个数据的通道
    v := 12
    ch <- v // 将 v 的数据发送到通道 ch
    s := <-ch // 从 ch 中接收数据,并将数据赋给 s

range

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在 map 集合中返回 key-value 对。

流程控制

switch if else for

流程控制语句和其他语言基本一样,这里不多赘述。唯一的区别就是 Go 的表达式不用加括号,例如:

switch i {
        case 0:
        ...
    }

    if i==1 {
    ... 
    }

    for i := 1; i < 12; i++ {
        ... 
    }

defer

defer 是 Go 语言一个很强大的关键字,在函数返回之前会调用该函数。可以很方便地用来进行资源回收。在一个函数中有多个 defer 函数时,其调用顺序又是如何呢?defer 函数是通过栈来管理的,所以其调用顺序自然就是后进先出。例如:

func f() int {
    var i int
    defer func() {
        i++
        fmt.Println("a defer2:", i) // 先入栈,后执行
    }()
    defer func() {
        i++
        fmt.Println("a defer1:", i) // 后入栈,先执行
    }()
    return i // 最后返回
}
补充:defer、return、返回值三者的执行顺序应该是:return最先给返回值赋值;接着defer开始执行一些收尾工作;最后RET指令携带返回值退出函数。不理解没关系,先记住defer在返回值前执行就可以了,初学不要过度陷入细节,我们后面遇到了再来剖析细节,这样学习曲线会降低很多。

多return

Go 语言可以返回多个返回值,类似这样:

func divide (a int, b int) (num int, err error){ //定义两个返回值

    if b == 0 {
        err = errors.New("被除数不能为零!")
        panic("被除数不能为0") //抛出异常被捕获,不能抛出没有被捕获,程序就会被中止
        return
    }
    return a / b, nil   //支持多个返回值
}

func main() {
    defer func() {
        if r := recover(); r != nil { // 用于捕获异常,如果有异常,则recover不会为空,进行捕获然后处理
            fmt.Println("recovered in function", r)
        }
    }()
    fmt.Println("Calling divide")
    fmt.Println(divide(1,0))
}

类型处理

Go 语言提供了一种在不同但相互兼容的类型之间相互转换的方式,这种转换非常有用并且是安全的。但是需要注意的是在数值之间进行转换可能造成精度丢失或者出现错误的情况。 一般转换语法如下: type_name(expression) 例如:

var i int
    i = 32
    var f float32
    f = float32(i) // 将int类型的变量转换成float32

错误处理

错误处理是任何语言都需要考虑到的问题,而 Go 语言在错误处理上解决得更为完善,优雅的错误处理机制是 Go 语言的一大特点。

Go 语言引入了一个错误处理的标准模式,引入 error 接口,该接口定义如下: type error interface { Error() string }

对于大多数函数,如果要返回错误,可以将 error 作为多返回值的最后一个。

panic 抛出异常,recover 用于捕获异常。就类似于 Java 里的 try catch 和 throw 捕获和抛出异常。这里也给出一个小例子,看看 Go 一般如何进行错误处理。

func devison(a, b int) (float32){
    var result float32
    if b == 0 {
        panic("除数不能为 0") // 抛出一个异常,如果不recover的话,程序就会中止
    }
    result = float32(a / b)
    return result

}

func main() {
    defer func() {
        if r := recover(); r != nil { // 用于捕获异常,如果有异常,则recover不会为空,进行捕获然后处理,使得程序可以继续往下执行,而不会因为panic而中止
            fmt.Println("recovered in function", r)
        }
    }()
    fmt.Println("Calling devision")
    fmt.Println(devison(12,2))
    fmt.Println(devison(1,0))
}
 hello world

本文首发于「阿星闲谈」微信公众号。