GO语言基础 | 青训营笔记

407 阅读13分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

前言: 

Go语言有时候被描述为“C类似语言”,或者是“21世纪的C语言”。Go从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想,还有C语言一直所看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。

 同时Go语言还有其自身的多种特点: 1. 高性能、高并发 2. 语法简单 3. 丰富的标准库 4. 完善的工具链 5. 静态链接 6. 快速编译 7. 跨平台 8. 垃圾回收 

重点内容:

 变量与常量、选择语句、循环语句、数组、切片、Map、函数、指针、结构体、结构体方法、错误处理、Go项目实战。

学习基础语法

Hello World

package main
​
import (
    "fmt"
)
​
func main(){
    fmt.Println("hello world")
}

以上是使用 Go 语言输出 Hello World 的代码。可以看出,Go 语言的入口点是 main 函数.

应当注意到,在 Go 语言中,;不是必要的,当一行中只存在一个语句时,则不必显式的为语句末添加 ;

``

变量

Go 语言的变量是类型后置的,可以这样创建一个类型为 int 的变量:

var a int = 1

允许在同一行声明多个变量:

var b,c int = 1, 2

Go 支持变量类型自动推断,也就是说,当我们立即为一个变量进行初始化时,其类型是可以省略的:

var d = true

相反,如果我们未为一个变量初始化,则必须显式指定变量类型,此时,变量会被以初始值自动初始化:

var e float64 // got 0

可以通过 := 符号以一种简单的方式(也是实际上最常用的方式)声明一个变量:

f := 3.2 // 等价于 var f = 3.2

最后,可以使用 const 关键字代替 var 关键字来创建一个常量(不可变变量):

const h string = "constant"

选择语句

Go 支持 ifelse ifelseswitch 进行选择控制。

if 7%2 == 0 {
    fmt.Println("7 is even")
} else {
    fmt.Println("7 is odd")
}
if num := 9; num < 0 {
    fmt,Println(num,"is negative")
} else if num < 10 {
    fmt.Println(num, "has 1 digit")
} else {
    fmt.Println(num, "has mutiple digits")
}

其他语言中,if(其他类似)后应当紧跟一个括号,括号内才是表达式,但是在 Go 中,这个括号是可选的,我们也建议不要使用括号。

要注意的是,if 表达式后面的括号是必需的,即使是对于单行语句块,您也必须添加括号,而不能像其他语言那样直接省略。

a := 2
switch a {
    case 0, 1:
        fmt.Println("zero or one")
    case 2:
        fmt.Println("two")
    default:
        fmt.Println("other")
}

这便是最简单,也是和其他语言最相似的 switch 语句,对一个 a 变量进行扫描,并根据不同的值输出不同的字符串。

当然,你也可以直接省略 switch 后的变量,来获得一个更加宽松的 switch 语句:

t := time.Now()
switch {
    case t.Hour() < 12:
        fmt.Println("It's before noon")
    default:
        fmt.Println("It's after noon")
}

需要注意的是,与其他语言恰好相反,switch 语句中每个 case 的 break 是隐式存在的,也就是说,每个 case 的逻辑会在执行完毕后立刻退出,而不是跳转到下一个 case

要想跳转到下一个 case,则应该使用 fallthrough 关键字:

v := 42
switch v {
case 100:
    fmt.Println(100)
    fallthrough
case 42:
    fmt.Println(42)
    fallthrough
case 1:
    fmt.Println(1)
    fallthrough
default:
    fmt.Println("default")
}
// Output:
// 42
// 1
// default

需要注意的是,fallthrough 关键字只能存在于 case 的末尾,也就是说,如下做法是错误的:

switch {
case f():
    if g() {
        fallthrough // Does not work!
    }
    h()
default:
    error()
}

但是,你可以使用 goto + 标签的方式来变相的解决这个问题。但是由于 goto 无论在任何语言的任何地方都应当是不被推荐使用的语法,因此此处不作继续探讨。想要继续了解的可以前往 Go Wiki 查看。

循环语句

在 Go 语言中不区分 for 和 while。你可以通过这样的方式创建一个最普遍的 for 语句:

for j := 7; j < 9: j++ {
    fmt.Println(j)
}

或者,将 for 语句中的三段表达式改为一个布尔值表达式,即可得到一个类似于其它语言的 while 语句:

i := 1
for i <= 3 {
    fmt.Println(i)
    i = i + 1
}

又或者,不为 for 语句填写任何表达式,你将得到一个无限循环,除非使用 break 关键字跳出循环,否则这个循环永远也不会停止,这看起来有些类似于 Java 的 while(true) {} 或是 Rust 的 loop {}

for {
    fmt.Println("loop")
}

当然,我们也可以使用 for range 循环的方式来遍历一个数组,切片,集合乃至映射(Map)。

当我们使用 for range 语句遍历一个数组,切片或是集合的时候,我们将得到该集合元素的索引(idx)和对应值(num):

nums := []int{2, 3, 4}
sum := 0
for idx, num := range nums {
    fmt.Println("range to index:", idx)
    sum += num
}
// Will got following output:
// range to index: 0
// range to index: 1
// range to index: 2
// sum: 9
fmt.Println("sum:", sum)

或者,当我们遍历一个 Map 时,将得到键(k)和值(v):

m := make(map[string]int)
    m["hello"] = 0
    m["world"] = 1
    // If key and value both needed
    for k, v := range m {
        // Will got following output:
        // key: hello, value: 0
        // key: world, value: 1
        fmt.Printf("key: %v, value: %v\n", k, v)
    }
    // Or only need key
    for k := range m {
        // Will got following output:
        // key: hello
        // key: world
        fmt.Printf("key: %v", k)
    }

如果我们不需要循环中的某个值,则可以使用 _ 符号代替变量名来遮蔽该变量(其他语言也有类似的做法,但是在 Go 中,此操作是必须的,因为未被使用的变量或导入会被 Go 编译器认为是一个 error):

// When only `v` variable needed
for _, v := range m {
    //... 
}

Go 语言没有 do-while 循环或其平替。可以通过这种方式手动编写一个近似的 do-while 循环:

for {
    work()
    if !condition {
        break
    }
}

很显然,break 和 continue 都是支持的,其用法和其他语言完全相同,在此直接略过。

数组,切片和映射

数组

可以使用以下方式声明一个指定长度的数组:

var a [5]int
a[4] = 100

声明了一个名为 a ,大小为 5 的 int 数组,并将其最后一个元素的值设置为 100

直接使用 := 进行声明当然也是可行的:

b := [5]int{1, 2, 3, 4, 5}

声明了一个名为 b,大小为 5,数组内元素初始值为 1,2,3,4,5 的 int 数组。

当然,多维数组也是可以的:

var twoD [2][3]int

创建了一个名为 twoD 的二维数组。

值得一提的是,当一个数组未被显式初始化元素值时,将采用元素默认值填充数组。

可以这样使用索引从数组中取出一个值:

fmt.Println(b[4]) // 5

当我们试图访问一个超出数组长度的索引,编译器将会拒绝为我们编译,并返回一个编译错误:

fmt.Println(b[5]) // error: invalid argument: index 5 out of bounds [0:5]

切片

数组是定长的,因此在实际业务中使用的并不是很多,因此,更多情况下我们会使用切片代替数组。

就像它的名字一样,切片(slice)某个数组或集合的一部分,切片是可变容量的,类似于 ArrayList,当切片容量不足时,便会自动扩容然后返回一个新的切片给我们。

可以使用如下方式声明一个切片:

s := make([]string, 3)

声明了一个长度为 3,容量为 3 的 string 切片。

切片的类型标识看起来和数组很像,但是实际上他们是不同的东西。切片并不需要在 [] 内指定一个长度,而数组是需要的。

需要注意的是,切片的 长度(length)  和 容量(capacity)  是两个完全不同的东西,前者才是切片实际的长度,后者则是一个阈值,当切片长度达到该阈值时才会对切片进行扩容。

当然,也可以直接指定一个切片的长度和容量:

s2 := make([]string, 0, 10)

创建了一个长度为 0 ,容量为 10 的 string 切片。

可以直接像数组一样为切片元素赋值:

s[0] = "a"
s[1] = "b"
s[2] = "c"

也可以使用 append 方法为数组添加新的元素:

s = append(s, "d")
s = append(s, "e", "f")

并返回更新后的切片。

可以使用 copy 方法将一个切片内的元素复制到另一个切片中:

c := make([]string, len(s))
copy(c, s)

使用 len 方法获得一个数组,切片的长度。

可以使用和数组相同的方式从切片中获得一个值:

fmt.Println(s[5])

但是不同的是,当我们试图越界访问一个切片时,编译器并不会给我们一个错误(因为切片的长度是不确定的),然而,这会得到一个 panic,并使程序直接结束运行:

fmt.Println(s[6]) // panic: runtime error: index out of range [6] with length 6

可以使用以下切片操作从数组和切片中截取元素:

fmt.Println(s[2:5]) // [c d e]

将返回一个新的切片,该切片的元素是 s 切片的第 2 个元素到第 4 个值(左闭右开)。

注意,在这种切片操作中,: 左边和右边的数字均可被省略,也就是说:

fmt.Println(s[:5]) // [a b c d e]

将返回切片第 0 个元素到第 4 个元素的切片。

fmt.Println(s[2:]) // [c d e f]

将返回切片第 2 个元素到最后一个元素的切片。

fmt.Println(s[:]) // [a b c d e f]

将返回切片的整个切片(副本)。

映射

映射(Map)是一个无序 1 对 1 键值对。可以使用如下方式声明一个 Map:

m := make(map[string]int)

声明了一个键(key)为 string 类型,值(value)为 int 类型的 Map。

当然,也可以提前初始化 Map 内的值:

m2 := map[string]int{"one" : 1, "two" : 2}

可以使用类似于数组和切片的赋值语法为 Map 赋值,只不过,将索引换成了 key,目标值换为了 value

m["one"] = 1
m["two"] = 2

使用 len 方法获得一个 Map 内包含键值对的长度。

fmt.Println(len(m)) // 2

可以使用和数组和切片类似的方式从切片中获得一个值,只不过,将索引换成了 key

fmt.Println(m["one"]) // 1

但实际上,这种写法是非常不好的,因为,当我们试图访问一个不存在的 key,那么 Map 会给我们返回一个初始值:

fmt.Println(m["unknown"]) // 0, wtf?

因此,我们需要接收第二个值 —— 一个布尔值,来判断该键是否在 Map 中存在:

r, ok := m["unknown"]
fmt.Println(r, ok) // 0 false

最后,使用 delete 函数从一个 Map 中移除指定的键:

delete(m, "one")

函数,指针,结构体与结构体方法

函数

可以通过这种语法声明一个带参有返回值函数:

func add(a int, b int) int {
    return a + b
}

声明了一个名为 add,拥有两个类型为 int,名称分别为 a 和 b 的形参,返回值为 int 的函数。

如果不需要返回值,则可以直接省略,就像 main 函数那样:

func main() {
    // ...
}

指针

Go 语言支持指针操作,但默认情况下(不考虑 unsafe),指针必须指向一个合法对象,而不是一个可能不存在的内存地址,你也不能使用指针进行地址运算(因此,与其说指针,不如称之为引用更加合适):

func add2(n int) {
    n += 2
}
​
func add2ptr(n *int) {
    *n += 2
}
​
func main() {
    n := 5
    add2(n) // not working
    fmt.Println(n) // 5
    add2ptr(&n)
    fmt.Println(n) // 7
}

使用 *type 声明一个指针变量,使用 * 对一个变量进行解引用,使用 & 获取一个变量的指针(引用)。

支持指针的 Go 也侧面印证了,默认情况下,Go 的方法传参均为传值,而不是传引用,如果不传入指针而直接传入一个值的话,则方法实参会被复制一份再传入。

结构体

Go 不是一门面向对象(OO)的语言,因此,Go 并没有类(Class)或是其他类似概念,取而代之的,是同类语言中均拥有的结构体(Struct)  。

使用如下方式来声明一个结构体:

type user struct {
    name     string
    password string
}

然后,使用如下方式初始化一个结构体:

a := user{name: "wang", password: "1024"}
fmt.Printf("%+v\n", a) // {name:wang password:1024}

如果未对一个结构体进行初始化,则结构体成员将采用默认值:

var b user
fmt.Printf("%+v\n", b) // {name: password:}
复制代码

可以使用 . 来访问结构体成员

fmt.Println(a.name) // wang
fmt.Println(a.password) // 1024

结构体方法

如果将函数类比为 Java 中的静态方法,那么结构体方法则可以类比为 Java 中的非静态方法(类成员函数)。

使用如下方式声明一个用于检查用户密码是否匹配的方法:

func (u user) checkPassword(password string) bool {
    return u.password == password
}

使用如下方式声明一个用于重置用户密码为指定值的方法(注意此处结构体是一个指针,只有这样才可以避免值拷贝,修改原结构体):

func (u *user) resetPassword(password string) {
    u.password = password
}

然后即可直接调用:

a.resetPassword("2048")
fmt.Println(a.checkPassword("2048")) // true

Go 错误处理

与 Java 不同,Go 语言并不支持 throwtry-catch 这样的操作,与 Rust 比较类似,Go 通过跟随返回值返回返回错误对象来代表方法执行中是否出现了错误 —— 如果返回的值错误对象为 nil,则代表没有发生错误,函数正常执行。

但是,由于 Go 并没有 Rust 那么强大的模式识别,因此,其错误处理并不能像 Rust 那样便捷有效,并时常饱受诟病(经典的if err != nil

以下方法试图从一个 user 切片中查找是否存在指定名称的 user,如果存在,则返回其指针,否则,返回一个错误。

要实现此功能,需要导入 errors 包:

import (
    "errors"
)

声明函数:

func findUser(users []user, name string) (v *user, err error){
    for _,u := range users {
        if u.name == name {
            return &u, nil
        }
    }
    return nil, errors.New("not found")
}

findUser 函数返回了多个值,这样,我们便可以创建两个变量直接接收它们(类似于 ES6 或 Kotlin 的 解构赋值 语法)。

调用函数:

func main(){
    u, err := findUser([]user{{"wang", "1024"}}, "wang")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(u.name) // wang
    
    if u, err := findUser([]user{{"wang", 1024}}, "li"); err != nil {
        fmt.Println(err) // not found
        return
    } else {
        fmt.Println(u.name)
    }
}

当函数执行完毕后,我们便可通过判断 err 是否为 nil 来得知错误是否发生,然后进行下一步操作。

Go 语言实战

在这一部分,通过三个简单的小项目带领学生学习了 Go 语言语法及其标准库使用:一个经典的猜数字游戏,给定一个随机数,让用户猜测这个数并给出与这个数相比是大了还是小了;一个在线词典,通过 HTTP 爬虫爬取其他在线词典网站的结果并返回;一个 SOCKS5 代理,简单的实现了 SOCKS5 的握手流程,并给予回答。

引用

  • GO语言圣经

  • 【Go入门】Go语言基础知识(CSDN)

  • Go语言教程|菜鸟教程 

总结

Go是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。Go的语法接近C语言,但对于变量的声明有所不同,Go支持垃圾回收功能。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。