GO语言趣学指南-Notes

预备

在 Linux 中编译 .go 文件:

1
go build name.go

将编译好的 go 二进制安装到系统中:

1
go install

示例代码:

1
2
3
4
5
6
7
8
9
package main

import (
"fmt"
)

func main() {
fmt.Println("Hello, palyground")
}

package 声明该文件代码 所属的包 , 如这里包的名字就是 main.

import() 导入所需的包, 可以连续导入多个包, 每个似乎占一行即可.

func name() {} 定义函数.

调用包内函数用 .

注意, main 函数, 在运行 GO 程序的时候, 它总是从 main 包的 main 函数开始运行. 如果 mina 函数不存在, GO 编译器会报错.

唯一允许的大括号放置风格

左大括号 {func 关键字位于同一行, 右大括号 } 独占一行.
(不仅是 func, if 等语句也相同)

如:

1
2
3
func main() {
...
}

这种风格的原因为: 方便编译器自动为每条语句添加分号 ;.

注释

// 做单行注释.

/**/ 做多行注释.

1 命令式编程

fmt.Println 会自动换行, 同 Perl 语言的 say.

fmt.Print 不会自动换行.

格式化输出用 fmt.Printf, 用法和 C 相同.

常量和变量

常量用 const 声明.

变量用 var 声明.

可以一次声明多个变量:

1
2
3
4
5
6
var (
distance = 56000000
speed = 100800
)
// or
var distance, speed = 56000000, 100800

操作符

不支持前置增量操作, ++count

引入子模块

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main 

import (
"fmt"
"math/rand"
)

func main() {
var num = rand.Intn(10) + 1
fmt.Println(num)
num = rand.Intn(10) + 1
fmt.Println(num)
}

2 循环和分支

预声明 Boolean 变量

  • true, 唯一真值
  • false, 唯一假值

也就是说 1 不算真值, 0 也不算假值.

比较运算符

比较字符串和数字用同一套. 但是不允许数字和字符进行比较.

if 和 else if

如:

1
2
3
4
5
6
7
if () {
...
} else if () {
...
} else {
...
}

可以不添加括号 ()

逻辑运算符

  • &&, 逻辑与
  • ||, 逻辑或
  • !, 逻辑非

switch

1
2
3
4
5
6
7
8
9
10
switch variable {
case "case 1":
...
case "case 2", "case 3":
...
case "case 4":
...
default:
...
}

不会自动下降, 需添加 fallthrougn:

1
2
3
4
5
6
7
8
9
10
11
switch variable {
case "case 1":
...
fallthrougn
case "case 2", "case 3":
...
case "case 4":
...
default:
...
}

注意这里的 case 不需要 {}.

variable 处也可以添加括号 ().

caseswitch 不一定要在同一列.

循环

1
2
3
4
5
for count > 0 {
fmt.Println(count)
time.Sleep(time.Second)
count--
}

可以用 break 跳出循环.

也可以为:

1
2
3
4
var count = 0
for count = 10; count > 0; count-- {
fmt.Println(count)
}

3 变量作用域

简短声明

1
2
var count = 10
count := 10

这两行等效.

若要在 for 循环时声明变量, 则需要用简短声明:

1
2
3
for count := 10; count > 0; count-- {
fmt.Println(count)
}

(同 if, switch 等)

4 实数

在不指定类型时, Go 编译器会自动推断.

如:

1
days := 365.2425

Go 会把所有带小数点的数字设置为 float64 类型.

显示声明的语法:

1
var answer float64 = 42

单精度浮点数用 float32

零值

每种类型的默认值都称为 零值 (zero value)

注意:

1
2
3
test := 0.1
test += 0.2
fmt.Println(test == 0.3)

结果为 false, 因为 test 的实际值为 0.3000000000004

5 整数

整数类型包括:

  • int
  • uint

还有如:

  • int8
  • uint8
  • int16
  • uint16

等.

查看变量类型

1
2
year := 2018
fmt.Printf("Type %T for %v\n", year, year)

%b 可以以二进制的形式打印出整数值:

1
2
var green uint8 = 3
fmt.Printf("%08b\n", green)

6 大数

处理很大的数字用 big 包.

7 多语言文本

声明字符串类型用 string, 如:

1
var test string = "Hello"

用反引号包裹的字符串和 Perl 中用单引号包裹类似. (可以很方便输出多行文本)

字符, 代码点, 符文和字节

两个类型:

  • rune ( int32 的别名)
  • byte ( uint8 的别名)

拉弦

可以将不同的字符串赋值给同一个变量, 但无法对字符串本身进行修改:

1
2
peace := "test"
peace = "hahaha"

(是可以的)

[] 访问字符串中的特定字符:

1
2
message := "test"
c := message[2]

8 类型转换

在 Go 中变量类型不能混合使用.

如:

1
2
age := 41
marsAge := float64(age)

12 函数

声明如:

1
func Intn(n int) int

调用如:

1
num := rand.Intn(10)

注意, 在 Go 中, 以大写字母开头的函数, 变量以及其他标识符都会被导出并对其他包可用. (以小写字母开头的函数无法从外部访问)

返回多个值的声明:

1
func Atoi(s string) (i int, err error)

可变参数

在变量类型前添加 ...:

1
func Test (test ...int) int

返回值还是用 return

编写函数

注意, 在同一个包中声明的函数在调用时不需要加上包名作为前缀.

13 方法

声明新类型 (注意不是给类型起别名, 属于不同类型了), 用 type:

1
2
3
type celsius float64
var temperature celsius = 20
fmt.Println(temperature)

通过方法为类型添加行为

Go 提供了方法, 但是没有提供类和对象.

可以将方法和同一个包中声明的任何类型相关联.

声明方法:

1
2
3
4
type test int
func (t test) Test() test {
...
}

这里 func 关键字之后的称为 接收者 , 每一个方法有且只能有一个接收者, 接收者的类型即绑定的类型.

14 一等函数

在 Go 语言里, 函数是 一等值 , 即: 可以将函数赋值给变量, 可以将函数传递给函数, 也可以编写创建并返回函数的函数.

将函数赋值给变量

如:

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"

func Tovar() {
fmt.Print("Hello function")
}

func main() {
test := Tovar
test()
}

将函数传递给其他函数

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"

func Test() int {
fmt.Print("Hello function")
return 10
}

func Getfunc (test func() int) {
test()
}

func main() {
Getfunc(Test)
}

有返回值则需要指出.

声明函数类型

如:

1
type testfunc func() test

将一个不接受任何参数, 返回值为 test 类型的函数的类型令为 testfunc.

则声明一个需要以函数作为参数的函数为:

1
2
3
func Getfunc (tf testfunc) {
...
}

等价于:

1
func Getfunc (tf func() test)

闭包和匿名函数

将匿名函数赋值给变量:

1
2
3
4
5
6
7
8
9
package main
import "fmt"

func main() {
test := func() {
fmt.Print("Hello")
}
test()
}

直接调用一个匿名函数:

1
2
3
4
5
6
7
8
package main
import "fmt"

func main() {
func() {
fmt.Print("Hello")
} ()
}

15 数组

如:

1
var test [8]string

同一个数组中的每个元素都具有相同的类型.

查看数组长度

len (内置函数) 查看数组长度.

1
length := len(test)

初始化

1
test := [2]string{"test1", "test2"}

(也就是用大括号 {})

自动计算数组长度

... 如:

1
2
3
4
5
6
arrtest := [...]string {
"test1",
"test2",
"test3",
"test4",
}

注意结尾的逗号 , 不能省略

二维数组

如:

1
var arrtest [5][5]string

16 数组切片

注意切片和数组是两种不同的类型.

如:

1
partarray := array[0:4]

获得一个跟底层数组具有同样元素的切片:

1
test := array[:]

直接创建一个切片 (好处在于不用指定长度):

1
test := []string{"test1", "test2"}

传递给一个函数切片时, 也不用指定长度:

1
2
3
4
5
6
7
func TestArray(ta []string) {
...
}
func main() {
arr := []string{"test1", "test2"}
TestArray(arr)
}

在 Go 中, 很多时候都使用切片而不是数组

带有方法的切片

给切片绑定方法:

1
2
type StringSlice []string
func (test []string) Sort()

17 更大的切片

给切片追加元素

append 函数:

1
2
test := []string{"test1", "test2", "test3"}
test = append(test, "test4")

当切片容量不足以执行 append 时, 才会创建新数组并复制旧数组中的内容.

长度和容量

len 函数查看切片长度, 用 cap 查看切片容量.

切片的容量是按倍数增长的

当切片容量改变时, 则会分配新的数组.

三索引切片操作

第三个参数用于指定容量:

1
test := test[0:4:4]

用 make 函数对切片实行预分配

make 指定切片的长度和容量:

1
test := make([]string, 0, 10)

前者为长度, 后者为容量.

18 映射 (即字典, 哈希)

如:

1
2
3
4
test := map[string]int{
"test1": 1,
"test2": 2,
}

即, 以 string 类型为键, int 类型为值.

如果键不存在, 则返回相应类型的零值.

“逗号与ok” 语法

1
2
3
if moon, ok := temperature["Moon"]; ok {
...
}

用 make 为映射预分配

如:

1
temperature := make(map[string]int, 8)

遍历映射

1
2
3
for key, value := range dictionary {
...
}

19 结构

声明:

1
2
3
4
var curiosity struct {
lat float64
long float64
}

访问, 用 . 点标记法:

1
curiosity.lat = -4.58

复用结构

1
2
3
4
type location struct {
lat float64
long float64
}

初始化

1
test := location{lat: -0.22, long: 2.11145}

或直接:

1
test := location{-0.22, 2.11145}

结构切片

1
2
3
4
5
6
7
8
type location struct {
name string
lat float64
}

locations := []location{
...
}

17 Go 没有类

Go 没有为构造器提供特殊的语言特性, 单纯利用函数即可.

构造器的命名惯例为: newtypeNewType

18 组合与转发

组合, 指用不同的组件来描述一个行为. 如你有描述走路, 跑步, 学习的函数, 可以通过这些组件来描述学生, 也可以用来描述一个机器人, 而不同于类, 将函数绑定在一个类别上.

转发, 指让一个类别能使用的方法, 也能让另一个类别使用.

自动转发

将类型嵌入到结构的定义中:

1
2
3
4
5
type report struct {
sol int
temperature
location
}

如这里的 temperaturellocation, 只写类型, 不指定字段名.

此时 report 类型就能使用 temperaturelocation 的方法.

虽然这里没有指明字段名, 但结构会自动为被嵌入的类型生成同名的字段:

1
fmt.Printf("average %vo C\n", report.temperature.average())

嵌入不仅会转发方法, 还能够让外部结构直接访问内部结构中的字段.
(感觉就是隐藏了 .temperature 这种)

18 接口

用于写入的接口 Writer

接口类型

接口通过列举类型必须满足的一组方法来声明

如:

1
2
3
var t interface {
talk() string
}

这里列举出的方法为 talk() string

若一个类型绑定了一个名为 talk, 返回值类型为 string 的方法, 其就可以被赋值给 interface.

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type martian struct {}

func (m martian) talk() string {
return "nack nack"
}

type laser int

func (l laser) talk() string {
return strings.Repeat("pew", int(1))
}

type talker interface {
talk() string
}

var t talker

t = martian{}
fmt.Println(t.talk())

t = laser(3)
fmt.Println(t.talk())

一般接口类型的名称以 -er 为后缀.

19 指针

Go 的指针操作和 C 语言相同:

  • &, 为地址操作符 (取址)
  • *, 接引用 (提供内存地址指向的值)

注意, go 中不允许 address++ 这样的指针运算操作.

声明如:

1
var test *string

指向结构体的指针

1
2
3
4
5
6
7
8
9
type person struct {
name string
age int
}

tim := &person{
name: "Tim",
age: 10,
}

指向数组的指针

1
power := &[3]string{"flight", "invisibility", "super strength"}

20 nil

nil 是 Go 中的一种零值, 如指针, 切片, 映射和接口等.

对空指针解引用常常会导致程序崩溃.

21 错误处理

Go 的错误处理利用其返回多个值的机制.

在没有发生错误的情况下, 函数返回的错误值为 nil

如:

1
2
3
4
5
files, err := ioutil.ReadDir(".")
if err != nil {
fmt.Println(err)
os.Exit(1)
}

关键字 defer

defer 关键字能保证操作会在函数返回之前触发:

1
defer f.Close()

这里保证了文件能被关闭.

创造性的错误处理

如:

1
2
3
4
5
6
7
8
9
10
11
12
type safeWriter struct {
w io.Writer
err error
}

func (sw *safeWriter) writeln(s string) {
if sw.err != nil {
return
}

_, err = fmt.Fprintln(sw.w, s)
}

新的错误

errors 包来生成一个错误信息, 但这个错误信息不会导致程序退出, 如:

1
2
3
4
5
6
7
8
func (g *Grid) Set(row, column int, digit int8) error {
if !inBounds(row, column) {
return errors.New("out of bounds")
}

g[row][column] = digit
return nil
}

这里的 error.New 生成一个错误.

有时, 可以先声明错误变量来保存错误信息:

1
2
3
4
var {
ErrBounds = errors.New("out of bounds")
ErrBounds = errors.New("invalid digit")
}

Panic

其会导致程序退出. 如:

1
panic("exit")

22 并发编程, goroutine 和 并发

Go 用 goroutine (不是一个关键字或函数, 只是表明 go 的 routine) 并发代码, 并用通道 (channel) 实现 goroutine 之间的通信.

启动一个 goroutine 只需加上 go 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
"time"
)
func main() {
go sleepGopher()
time.Sleep(4 * time.Second)
}
func sleepGopher() {
time.Sleep(3 * time.Second)
fmt.Println("... snore ...")
}

不止一个 goroutine

这里实际上不是都同时运行, 而是采用 分时 的方式.

通道

通道 (channel) 可以在多个 goroutine 之间传递值.

创建通道, 用 make 函数:

1
c := make(chan int)

这里创建了一个 int 类型的通道, 可用于发送和接收整数值.

<- (左箭头操作符) 用于接收和发送值.

发送如:

1
c <- 10

注意 , 发送操作会等待直到有另一个 goroutine 尝试对相同的通道执行接收操作为止.

接收如:

1
r := <-c

注意 , 接收操作会等待直到有另一个 goroutine 尝试对相同的通道执行发送操作为止.

用 select 处理多个通道

selectswitch 语句类似, 其包含的每一个 case 分支都持有一个针对通道的接收和发送操作.

time.After 函数会返回一个通道, 该通道会在经过特定时间之后接收到一个值.

如:

1
2
3
4
5
6
7
8
9
10
timeout := time.After(2 * time.Second)
for i := 0; i < 5; i++ {
select {
case gopherID := <-c:
fmt.Println("gopher ", gopherID, " has finished sleeping")
case <-timeout:
fmt.Println("my patience ran out")
return
}
}

阻塞和死锁

goroutine 被阻塞时并不消耗任何资源.

当一个或多个 goroutine 因为某些永远无法发生的事情而被阻塞时, 被称为 死锁, 出现死锁的程序通常会崩溃或被挂起.

23 并发状态

把多个 goroutine 争相使用值的情况称之为 竞态条件

互斥锁

goroutine 可以利用互斥锁阻止其他 goroutine 在同一时间进行某些事情.

这里的 “互斥”, 指 “相互排斥”.

互斥锁LockUnlock 两个方法.

通过 sync 包引入.

使用如:

1
2
3
4
5
6
7
8
9
package main
import "sync"

var mu sync.Mutex

func main() {
mu.Lock()
defer mu.Unlock()
}

这里 sync.Mutex 声明的锁是一个 “未上锁的互斥


GO语言趣学指南-Notes
http://example.com/2023/09/26/GO语言趣学指南-Notes/
作者
Jie
发布于
2023年9月26日
许可协议