序言

本文是为了 《编译原理》期末大作业而准备的。 本文将讨论 Golang 的特点

直观 —— 奇怪而严格的语法

还记得在高中的时候,接触到 Golang 只觉得这个语言的语法很奇怪 例如变量声明,要使用 var 标识符声明,变量名竟然在类型名前面

1
var Variable int

而语法又莫名严格 例如如下语句是无法编译的:

1
2
3
4
5
func function(a int) error
{   // ERROR!
	// ...
	return nil
}

因为强制要求大括号不换行

如下语句也是不行的

1
2
3
4
5
a := [
	1,
	2,
	3 // error!
]

需要改成

1
2
3
4
5
a := [
	1,
	2,
	3, // end comma is nessceary
]

对于访问控制也是很奇怪,首字母大写就是 Public,小写就是 private

没有 Class —— 拥抱函数范式

存在一个类似 Class 的 struct

1
2
3
4
type User struct {
	ID uint64
	Name string
}

但是并没有 C++ 中所说的五大函数(析构函数、移动赋值、拷贝赋值、拷贝复制、移动赋值)

所以一般认为 Golang 不是面向对象的。

函数范式

将电脑运算视为函数运算,避免使用程序状态以及可变物件。

lambda 演算

语法名称描述
x变量用字符或字符串来表示参数或者数学上的值或者表示逻辑上的值
(λx.M)抽象化一个完整的函数定义(M是一个 lambda 项),在表达式中的 x 都会绑定为变量 x。
(M N)应用将函数M作用于参数N。 M 和 N 是 lambda 项。

函数作为 头等对象,一个函数既可以作为其他函数到输入参数值,也可以从函数中返回。

在其他语言中(例如 Python, C++) 存在 Lambda 表达式这一实现 闭包 的语法。

1
sum = x1, x2: x1 + x2
1
auto sum = [](int a, int b){return a + b}

在 Golang 中并不存在 lambda 表达式,而函数作为头等对象,可以作为变量、参数、返回值等等使用

1
2
3
4
5
6
7
8
var f func(int) int

func main() {
	f = func(x int) int {
		return x + x
	}
	fmt.Println(f(2)) // 4
}

一个将 string 类型转换为 int 类型的实例如下

1
2
3
4
5
6
7
8
9
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
id_string := scanner.Text()
var id int
id = func(str string) (i int) {
	i, _ = strconv.Atoi(str)
	return
}(id_string)
fmt.Println(id)

Golang 的编译器

Golang 是 自举 的。

计算机科学中,自举是一种自生成编译器的技术——也就是,某个编程语言编译器(或汇编器)是该语言编写的。

Golang 源代码通过四个步骤编译为可执行文件:

  1. 词法、语法分析
  2. 类型检查和 AST(Abstract Syntax Tree) 转换
  3. 通用 SSA (Static Single Assignment, 静态单赋值) 的中间代码生成
  4. 机器代码生成

静态单赋值可以减少重复赋值造成的浪费。

1
2
a := 123 // waste !
a = 234

Golang 使用 LALR(1)语法

交叉编译

Golang 可以进行交叉编译

Golang 生成的中间代码是平台无关的,可以生成不同的机器码(arm64, x86_64, WASM)

下面简单介绍一下 WASM WebAssembly,是一个在 栈虚拟机 上使用的二进制指令格式。设计目标是在浏览器上提供一种具有高可移植性的目标语言。

WASM 并不是用来代替 JS 的。

以下是一个 Golang 编写的 WASM 例程

1
2
3
4
5
6
7
8
9
// main.go
package main

import "syscall/js"

func main() {
	alert := js.Global().Get("alert")
	alert.Invoke("Hello, WebAssembly!")
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// index.html
<html>
<script src="static/wasm_exec.js"></script>
<script>
  const go = new Go();
  WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject)
    .then((result) => go.run(result.instance));
</script>

</html>

编译选项和依赖库

1
2
3
GOOS=js GOARCH=wasm go build -o static/main.wasm

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" static

增量编译

由于 Golang 要求每个源文件显式声明所属包、引入包,以及静态检查循环引入的机制 Golang的增量编译可以使 Golang 编译得极快。

静态类型 vs 动态类型

对于编程语言又一个 “强弱类型”的模糊概念。 js,python是弱类型的, c++, java 是强类型的。

而静态类型和动态类型是指,类型是否可以在执行时判断。因此一般而言,编译型语言多是强类型,而解释型语言多是弱类型。

存在的特例是 TypeScript。 TypeScript 是强类型的,但是在执行时没有类型(只有 js 的六个类型)

支持反射的语言(例如 Java、Golang)也在执行时提供类型判断。

Golang 存在一个神奇的 Any 类型。 分析源码可以发现

1
type Any interface{}

Interface

接口定义对象的行为。

 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
package main

import "fmt"

type Animal interface {
	Speak() string
}

type Dog struct{}
type Cat struct{}

func (d Dog) Speak() string {
	return "Wolf!"
}

func (c Cat) Speak() string {
	return "Mewo!"
}

func main() {
	animals := []Animal{Dog{}, Cat{}}
	for _, val := range animals {
		fmt.Println(val.Speak())
	}
}
// output:
// Wolf!
// Cat!
1
2
3
4
5
6
7
func (c Cat) Purr() {
	return "Purr!"
}

dog.Purr() // ERROR
animal := Animal(Cat{}) // which is a animal
animal.Purr() // ERROR

interface

需要注意的是 interface{} 虽然被称为 any,但是实际上它不是 any(或者说,根本上就应该认为 any 是另一个类型)

一切类型都实现了 interface{} 接口,因此所有类型都是 interface{}

1
2
3
func fun(a interface{}) {
	fmt.Println(a)
}

这个函数接受所有类型的参数,但是当然,其内部的处理也只能局限在 interface{}

使用反射,可以输出这个参数的类型

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

// import ...

func showType(a interface{}) {
	fmt.Println(reflect.TypeOf(a))
}
type A struct {
}

func main(){
	var a A
	fun(a) // main.A
}

golang 官方fmt库中通过 interface{} 实现的格式化打印十分强大, 在"fmt"库中有如下函数

 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
56
57
58
59
60
func Println(a ...any) (n int, err error) {
	return Fprintln(os.Stdout, a...) // 调用 Fprintln,将参数写入 Stdout
}

func Fprintln(w io.Writer, a ...any) (n int, err error) {
	p := newPrinter()
	p.doPrintln(a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

func (p *pp) doPrintln(a []any) {
	for argNum, arg := range a {
		if argNum > 0 {
			p.buf.writeByte(' ') // 如果多于一个参数,则在参数之间写入空格
		}
		p.printArg(arg, 'v') // 打印参数,并且有一个 v 的参数
	}
	p.buf.writeByte('\n') // 最后换行
}

func (p *pp) printArg(arg any, verb rune) {
	p.arg = arg
	p.value = reflect.Value{} // 反射

	if arg == nil { // 是 nil
		switch verb {
		case 'T', 'v':
			p.fmt.padString(nilAngleString) // "<nil>"
		default:
			p.badVerb(verb) // verb 不对,报错
		}
		return
	}
	switch verb {
	case 'T':
		p.fmt.fmtS(reflect.TypeOf(arg).String()) // 使用 reflect 返回字符串
		return
	case 'p':
		p.fmtPointer(reflect.ValueOf(arg), 'p') // 指针, 使用 reflect
		return
	}
	switch f := arg.(type) {
		// ... 对 不同类型的 arg 的处理,不需要反射
			default:
		if !p.handleMethods(verb) {
			// Need to use reflection, since the type had no
			// interface methods that could be used for formatting.
			p.printValue(reflect.ValueOf(f), verb, 0)
		}
	}
	}

// in reflect package
type Value struct {
	typ_ *abi.Type // 类型
	ptr unsafe.Pointer // 指针
	flag // type flag uintptr,字节操作
}

高并发 Goroutine

Go 支持 语言级 并发

进程、线程、协程

https://blog.f1nley.xyz/post/code/concurrency/

Goroutine

Go 语言的调度器通过使用与 CPU 数量相等的线程减少线程频繁切换的内存开销,同时在每一个线程上执行额外开销更低的 Goroutine 来降低操作系统和硬件的负载。

1
2
3
4
5
6
func fun() {
	for {
		// ...
	}
}
go fun() // 开一个协程

上下文机制

1
2
3
4
5
6
type Context interface {
	Deadline() (deadline time.Time, ok bool) // 这个上下文被取消的时间
	Done() <-chan struct{} // 当前工作完成或者上下文被取消的时候关闭
	Err() error // 错误处理
	Value(key interface{}) interface{} // 值
}

管道机制

channel 用于 goroutine 之间进行通信 下面给出一个优雅地结束服务器的例程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func main() {
	// ...
	go func() {
		if err := server.Listen(":8080"); err != nil {
			log.Fatalln(err)
		}
	}()
	quit := make(chan os.Signal)
	signal.Notify(quit, os.Interrupt, syscall.SIGTERM) // listen to the signals
	<-quit // main goroutine is blocked here
	log.Println("Shutting Down")
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // wait up to 5 secs to cancel
	defer cancel() // after shutdown
	if err := server.Shutdown(ctx); err != nil {
		log.Fatal("Server Shutdown", err)
	} else {
		log.Println("Server Exiting")
	}
}

高性能 GC

内存管理模式

  1. C/C++ 的手动内存管理 (malloc, new, free)
  2. Java, Python, Golang GC (Garbage Collective)垃圾回收机制
  3. Rust 所有权机制
  • 手动管理:看程序员个人水平,性能可能很高,也可能造成灾难性后果
  • GC:程序员无需关心GC问题,一定存在的性能消耗。

GC 存在“内存开销”和“运算开销”之间的矛盾。

Java 的 GC 机制被称为 STW (Stop The World)。在执行垃圾回收时,Java的所有线程被挂起,全局暂停。

Golang 的 GC 机制也是 STW,但是实现相当复杂。 Golang 的思路是尽可能降低 STW 造成的时延(微秒级),但是内存占用可能会较大。

Golang 使用 三色标记算法优化垃圾回收机制

三色标记算法

  • 白色对象 — 潜在的垃圾,其内存可能会被垃圾收集器回收;
  • 黑色对象 — 活跃的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象;
  • 灰色对象 — 活跃的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;
  1. 从灰色对象的集合中选择一个灰色对象并将其标记成黑色;
  2. 将黑色对象指向的所有对象都标记成灰色,保证该对象和被该对象引用的对象都不会被回收;
  3. 重复上述两个步骤直到对象图中不存在灰色对象;

对于开发者

1. gopls LSP Server

Golang 可以很方便的使用 gopls LSP 服务器提供编程时的协助

2. 社区环境

  • Google 亲儿子
  • Gin 轻量 Web 框架
  • GORM ORM 框架
  • sql/driver SQL 引擎
  • viper 配置文件解析

参考