初学者眼中的Go语言

最近花了一些时间过了一遍Go编程语言中的Go语言之旅, 也算是入门了, 下面谈谈我认知的Go语言

类型

不像绝大数语言,类型定义在了变量名之前,Go中类型在变量名之后,如下:

1
2
3
4
5
6
7
func add(x int, y int) int {  
return x + y
}
//等效于
func add(x, y int) int {
return x + y
}

官方也在博客Go’s Declaration Syntax中给出了理由, 但可能是由于惯性, 我仍然不是很喜欢这种设计🤥

关于变量初始化:

1
2
3
var i, j int = 1, 2
//等效于
var i, j = 1, 2

幸亏变量初始化时类型声明可以忽略, 不然真的看上去很怪, 忽略后一看, 这不是JavaScript吗🥰

Go没有while

绝大多数编程语言都是有while循环的, 虽然while循环的本质还是for循环, Go说: for是Go中的while

1
2
3
4
5
6
7
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}

defer

defer 语句会将函数推迟到外层函数返回之后执行。其他编程语言中没见过这个特性, 但在Go中, 它被广泛使用

1
2
3
4
5
6
7
8
9
func main() {
defer fmt.Println("world")

fmt.Println("hello")
}

//输出结果
hello
world

在实际使用上来说, 这有点像Java和Python中的finally, 但不同的是, finally是异常处理的一部分, 靠控制流机制实现, 而defer是通过栈机制实现的。推迟调用的函数调用会被压入一个栈中。 当外层函数返回时,被推迟的调用会按照后进先出的顺序调用。

切片

说到切片其实很容易让人想到Python, 它和Go中的切片有什么不同点呢?

  1. 通用性: Python中的切片操作非常通用,可以对列表、元组、字符串等进行切片。Go中的切片是一种具体的数据结构,只能用于切片类型。

  2. 语法: 两者语法相似, 都可以写做[a,b,c], 但Python中a,b,c的取值可以是负值, 而Go中不可以, 并且c这个位置在两种语言中的含义也不相同, Python中的c表示步长, 而Go中的c表示切片的容量。

  3. 返回值: Python切片操作返回一个新的对象,与原对象在内存中是分离的。而Go中的切片操作返回的是原切片的一个视图,共享相同的底层数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)

// 截取切片使其长度为 0
s = s[:0]
printSlice(s)

// 扩展其长度
s = s[:4]
printSlice(s)

// 舍弃前两个值
s = s[2:]
printSlice(s)
}

func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

//运行结果
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]


func main() {
var s []int
printSlice(s)

// 可在空切片上追加
s = append(s, 0)
printSlice(s)

// 这个切片会按需增长
s = append(s, 1)
printSlice(s)

// 可以一次性添加多个元素
s = append(s, 2, 3, 4)
printSlice(s)
}

func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}


//运行结果
len=0 cap=0 []
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=5 cap=6 [0 1 2 3 4]

切片的长度和容量之间的关系有以下几点:

  1. 切片的长度(len)总是小于或等于其容量(cap)。
  2. 当切片被创建时,长度和容量通常是相等的,但如果从一个较大的数组或切片创建一个新的切片时,新切片的容量可能会大于其长度。
  3. 当使用内建函数append向切片追加元素时,如果追加的元素超出了当前长度但未超出容量,切片的长度会增加,而容量保持不变。
  4. 如果append导致切片长度超过容量,Go运行时会分配一个新的底层数组,并将现有的元素和新元素复制到这个新数组中。这时,切片的容量会变为新的长度值,因为新数组的大小至少是新长度的两倍(具体增长策略可能更复杂,但至少是这个保证)。
  5. 切片可以通过重新切片(reslicing)来减少长度,但这样做不会改变其容量。重新切片的语法是slice[low:high],其中low是切片的起始索引,high是结束索引(不包括)。如果省略low,则默认从切片的开始;如果省略high,则默认到切片的末尾。

并发编程

Go的并发机制个人认为比Java要更容易上手, 协程和信道的设计加上go关键字启动, 使得并发变得更加简单

谈谈Go和Java的区别:

  1. 并发模型的本质: Java是基于线程的并发模型,其中每个线程都是操作系统的轻量级进程。Go语言采用了基于协程(goroutine)的并发模型。Goroutines是轻量级的线程,由Go运行时管理,而不是操作系统。

  2. 内存模型和同步: Java有一个明确定义的内存模型,它涵盖了变量的可见性、有序性和原子性。Java提供了多种同步机制,如synchronized关键字、Lock接口和volatile关键字,以确保并发安全。Go也有自己的内存模型,它通过goroutines和channels来保证数据的同步。Go鼓励使用channels进行通信,而不是传统的锁机制。这有助于减少竞争条件和死锁的问题。

  3. 运行时管理: Java的并发管理主要由JVM负责。Go的并发管理由Go运行时负责。

  4. 性能和可扩展性: Java线程的创建和上下文切换开销相对较高。因此,Java程序在创建大量线程时可能会遇到性能问题。goroutines的创建和上下文切换开销非常低,这使得Go程序能够轻松创建数以万计的goroutines。Go的运行时能够更有效地在多个OS线程上调度这些goroutines。