文章
What about Go?
本文目录 12
Go 语言
Go语法学习,from a cpp & python programmer’s perspective
Intro
Go语言是一个现代的开源编程语言;他被设计为一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。
对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率,所以它是一个对游戏服务端开发友好的语言
最典型也是最成功用Go开发的项目就是k8s
Go和C一样都是编译型的语言,性能接近C,同时编译速度比C快很多;但是由于GC机制的存在,Go几乎不用烦恼内存管理,由此他的开发效率是比c高的
语言特性
基础语法
语言结构
package main
import "fmt"
func main() {
/* 这是我的第一个简单的程序 */
fmt.Println("Hello, World!")
}
- 定义文件属于哪个package
- 导入package
- 可执行程序的接口main,类似c
数据类型&语言变量
数据类型
- bool
- 数字,包括int, float32 ,float64, uint等等
- 字符串
- 派生类别,包括Pointer, Array, Struct,Interface, slice, map, etc.
声明变量的一般形式是使用 var 关键字:
var identifier type
可以一次声明多个变量:
var identifier1, identifier2 type
也可以省略var和类型,用短变量声明:=让编译器自动推断类型;但是它不能用于全局变量,且他是一个声明操作而不是赋值操作
Constant 常量
const和其他编程语言一样,都是在程序运行时不会被修改的值
而Go因为没有enum 枚举关键字,所以通常使用常量组来实现枚举效果
const (
Unknown = 0
Female = 1
Male = 2
)
同时go里有一个特殊的常量,也被成为常量计数器iota
iota就是一个英文单词,意为极小量
// iota 从 0 开始,每行加 1
const (
a = iota // 0
b // 1(省略值,默认使用上一行的 iota)
c // 2package main
import "fmt"
// iota 从 0 开始,每行加 1
const (
a = iota // 0
b // 1(省略值,默认使用上一行的,在这个例子里是iota)
c // 2
)
func main() {
fmt.Printf("a=%d, b=%d, c=%d\n", a, b, c)
}
)
结果为a=0, b=1, c=2
运算符、条件语句、 循环语句、数组 略
分支语句
这里特地提一下分支,go使用select和case`关键字
select{
case x:
x = x + 1
case y:
y = y + 1
}
这里比较独特的一个点是,select会等待一个case的完成并进入,比如这里结合并行的一些东西
func 函数
func function_name( [parameter list] ) [return_types] {
函数体
}
变量的作用域和c相同
struct 结构体
type struct_variable_type struct {
member definition
member definition
...
member definition
}
// 使用{}进行初始化
// variable_name := structure_variable_type {value1, value2...valuen}
range
同python,对可以迭代的变量类型,如array,slice,channel,map进行循环迭代并返回索引+索引值/key-value对
slice 切片
go的切片还挺有特点的,和python的切片可以说是完全不一样
python的切片是值拷贝,开了一片新的内存来copy进去
如果从类似python的视角来切入,从一个已有的数组来初始化切片,那么go的切片相当于是对原数组的一个引用
改变切片中的元素,原数组中的元素也会发生对应的变化
a := [5] int {1,2,3,4,5}
b = a[1:4] # [2, 3, 4]
b[0] = 999
fmt.Println(a) // [1, 999, 3, 4, 5] (被改写了!)
fmt.Println(b) // [999, 3, 4]
但是其实slice提出的目的更多是作为一个动态数组被使用的,与数组相比,切片是可以追加和扩容的
可以把go的切片理解为一个结构体
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 长度
cap int // 容量
}
切片的声明是是var identifier [ ] type,和数组的区别就是中括号内是空白的;
要创建一个切片可以用make函数 slice1 := make ([ ]type, len, capacity)(cap是可选项),也可以原地初始化s:= [ ]int {1,2,3},其中len=cap=3;或者像上面一样用数组的切片引用初始化
得到一个切片之后,可以用和python类似的语法去做截取,也可以用copy(slice_new,slice_old)和slice1 = append(slice1,1)(append会返回一个slice,其可能是新的,取决于append需不需要扩容)
Interface 接口
Go 的接口设计简单却功能强大,是实现多态和解耦的重要工具
recap: 多态指同一个行为,不同的对象表现出不同的形态
第一次看到这个东西的时候感觉和“基类”+“虚函数”非常像
满足某个interface要求的所有方法的类,就认为是这个接口的一个实现;和基类定义一个虚函数,继承类override这个虚函数类似。针对编写接口的一个函数,可以传入接口的任何一个实现作为参数;而针对基类编写的函数,也能传入子类的对象作为参数
我能想到的,唯一的从语法上的区别,或者说其中一者能做而另一者不能做的,就是接口是只定义方法,而没有字段。而基类拥有自己的字段并可以独立的运作,或者说基类也是一个可以实例化的类(在没有纯虚函数的情况下)
而在逻辑上,基类(继承)体现的是一种是什么的关系,是这个类的ID card;接口(功能)体现的则是能做什么的关系,一个class可以实现无数个接口,他是这个类的类似于资格证书的东西
举个例子,假设你写了一个 Bird基类,它有 Fly() 方法。后来你发现,Airplane也能飞。如果不用接口,你只能让 Airplane 去继承 Bird。这在逻辑上是荒谬的,而且在技术上会带来飞机拥有“下蛋”、“羽毛”等无用的垃圾字段
而如果使用接口去定义一个 interface Flyable { Fly(); }。Bird 可以实现它,Airplane 也可以实现它。两者互不干扰,不共享任何无意义的数据
另外,go是 Duck Typing 的,即它不用显式的implement一个interface,而是只要你实现了这个interface的功能,在编译的时候会自动被视为一个接口的实现
package main
import "fmt"
// 1. 定义接口:只规定“能飞”,不包含任何数据
type Flyable interface {
Fly()
}
// 2. 麻雀:完全不知道接口的存在,只实现自己的方法
type Sparrow struct {
FeatherColor string
}
func (s Sparrow) Fly() {
fmt.Println("麻雀扑腾翅膀飞")
}
func (s Sparrow) LayEggs() {
fmt.Println("下了一个蛋")
}
// 3. 飞机:独立实现,不继承鸟的任何东西
type Airplane struct {
EngineType string
}
func (a Airplane) Fly() {
fmt.Println("飞机依靠引擎推力飞行") //实现了Fly,duck typing
}
// 飞机没有 LayEggs(),也没有 FeatherColor
func main() {
// 4. 关键:因为都有 Fly() 方法,两者自动成为 Flyable 类型
var flyables []Flyable
flyables = append(flyables, Sparrow{FeatherColor: "棕色"})
flyables = append(flyables, Airplane{EngineType: "涡轮风扇"})
// 5. 统一调用,各飞各的
for _, f := range flyables {
f.Fly()
}
// 输出:
// 麻雀扑腾翅膀飞
// 飞机依靠引擎推力飞行
}
Inheritance?Composition! 组合
既然Go是一个青睐interface的语言,那么自然它就不会再去费工夫实现一个类的继承,通过将一个结构体嵌入到另一个结构体中,子结构体就可以”继承”父结构体的字段和方法
但是这和c里拥有一个私有对象又是不一样的,组合是匿名的嵌入,只用声明一个类而没有实例化的对象,并且能获得这个对象的方法提升,下面展示了两者的区别:
type Writer struct {
Name string
}
func (w Writer) Write(data string) {
fmt.Println(w.Name, "is writing:", data)
}
type Logger struct {
w Writer // ❌ 这是私有字段,不是组合!
}
l := Logger{w: Writer{Name: "File"}}
l.w.Write("hello") // 必须通过 w 访问,麻烦!
type Logger struct {
Writer // ✅ 匿名嵌入,方法自动提升
}
l := Logger{Writer: Writer{Name: "File"}}
l.Write("hello") // ✅ 直接调用,仿佛 Logger 自己的方法!
可以看到l能直接调用Write函数了,也就是说组合可以得到所有的方法
但是这样的方法是没有类型的层次结构的,所以也就不符合面向对象编程的要求,事实上Go也并不完全是面向对象编程的
指针
从一个有c基础的人看来,go的指针无疑是简单的(其他所有语言的指针都是简单的)
这里先不浪费笔墨了
并发
并发应该算是Go最亮眼的特性了,Goroutine 是一种比操作系统线程轻量得多的执行单元——每个 Goroutine 的初始栈大小仅为约 2 KB,而操作系统线程通常在 1 MB 以上,你可以在一台普通机器上同时运行数十万个 Goroutine,这是传统线程模型无法做到的,也是他能在云服务器上被广泛使用的原因
go中创建一个goroutine并不不会像c那样创建一个全新的系统线程,而是在运行的时候通过调度器,把大量的goroutine调度到少量系统线程上
一些概念/单元:
Goroutines:
- Go 中的并发执行单位,类似于轻量级的线程
- 他的调度由 Go 运行时管理,用户无需手动分配线程
Channel:
- Go 中用于在 Goroutine 之间通信的机制
- 支持同步和数据共享,避免了显式的锁机制
- 使用
chan关键字创建,通过<-操作符发送和接收数据
Scheduler(调度器):
Go 的调度器基于 GMP 模型,调度器会将 Goroutine 分配到系统线程中执行,并通过 M 和 P 的配合高效管理并发。
- G:Goroutine
- M:系统线程(Machine)
- P:逻辑处理器(Processor)
Goroutine
语法:go function_name (params)
Channel
当进程之间共享变量的时候,可能会引发竞态(data race);而为了解决这种竞态,c中引入了种种机制比如mutex.condition variable等等,为了让共享的变量被操作的过程是原子化即不被打扰的。Go这里引入了一个试图“解决”竞态的工具channel,用来实现不同进程之间的通信,从而绕过共享变量可能带来的问题
Date race:
以
+1为例子,这个看似简单的操作,实则是由load,add,write等微小的汇编指令完成的;如果在第一条+1结束但是没有写回内存的时候,下一条+1读取到的值还是原来的值,会造成这个共享变量最终的值不是我们想要的
使用 make 函数创建一个 channel,使用 <- 操作符发送和接收数据。如果未指定方向,则为双向通道
ch := make(chan int) // 使用chan关键字船舰通道
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
func worker(ch chan int) {
result := 42
ch <- result //将结果传入通道
}
func main() {
ch := make(chan int)
go worker(ch)
result := <-ch //从通道拿到结果
fmt.Println(result)
}
以这里的result为例子,可以通过一个通道把一个routine里的值传出来。当然c的thread也可以返回一个值并赋给main里的某个变量,但是go的一个worker可以在退出前发送很多结果:
func worker(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
func main() {
ch := make(chan int)
go worker(ch)
for result := range ch {
fmt.Println(result)
}
}
C 的线程返回值不适合表达这种持续通信。如果用 C 实现,通常需要自己组合:
- 共享队列
- 互斥锁
- 条件变量
- 队列关闭标志
- 内存生命周期管理
值得注意的是,如果这个channal是没有缓冲的,那么这条goroutine会阻塞一直等待channel里的东西被取走之后再恢复,比如下面这段code
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
fmt.Println("sender: before send")
ch <- 42 //这里阻塞,等待被取走
fmt.Println("sender: after send")
}()
time.Sleep(2 * time.Second)
fmt.Println("receiver: ready to receive")
x := <-ch
fmt.Println("receiver: got", x)
time.Sleep(time.Second)
}
这里“sender: after send“会在“receiver: ready to receive”之后被打印(但是“sender: after send“和“receiver:got”的先后顺序是不一定的,取决与scheduler的行为)
async & sync
go里没有async.sync的关键词,当使用go function的时候,就相当于
Runtime Character
GC
C语言是手动malloc+free堆上的内存的,
关于malloc这部分的知识可以看另一篇博客
Reference
[菜鸟教程](Go 简介 | 菜鸟教程)