推荐视频 8小时转职Golang工程师(如果你想低成本学习Go语言)
文章目录
与C++对比
C++20引入了【协程】,但本身并不自带调度器,也没有像 Go 一样的原生 channel 和调度机制。(可借助第三方库)
虽然 C++ 功能强大、生态丰富,但 Go 的定位与目标用户群完全不同。Go 并不是为了取代 C++,而是为了解决 C++ 在某些场景下的“复杂性、效率开发难题”。
其他Go优点:
- 编译上:Go是模块编译
- 官方内置依赖管理
- Go原生自带GC自动管理内存
- Go 自带跨平台编译能力(在windows上编译Linux程序)
Go 默认会把所有依赖打包成一个单一的可执行文件,不依赖系统库、共享库、运行时环境等。 - 虽然 Go 没有虚拟机,但它有自己的 runtime,内嵌在最终编译生成的可执行文件中。(不像 Java 的 JVM 那样独立存在。)包括:协程调度器(M:N 的 goroutine 调度),内存管理(GC),通道通信(channel),时间管理(定时器、sleep 等),系统调用封装(syscall 层)。
Go语言
2009年,Google公开Go语言。
Go 的爆红,与它适合的场景有关:
- 云原生开发(Kubernetes 就是用 Go 写的)
- 微服务后端(如 Docker、etcd、Consul)
- 高并发网络服务
- DevOps 工具(如 Terraform、Prometheus)
基本语法
特殊类型与占位符
_
代表 匿名变量%T
类型
:=
//(只能在函数体内部使用,并且不能声明已存在的变量)
x := 10 // 自动推导类型为 int
name := "Tom" // 自动推导类型为 string
// 等价:
var x int = 10
var name string = "Tom"
data, ok :=
if data, ok := ; ok{}
defer
数组 slice切片
make([]int, 3, 5)
numbers[1:4]
map[string]string
struct 结构体
只有对外包的公开和隐藏,没有 private 等复杂属性。
type Book struct{
title string //属性名 小写只能在本包内访问
auth string
}
// this是调用该方法对象 的拷贝
func (this Book) Show(){ //方法名大写,对外可见(导出)
fmt.Println("Nmae = ", thie.Name)
}
func (this Book) GetTitle() string{
return thie.Name
}
func (this *Book) SetTitle(newTitle string){
this.Name = newTitle
}
//
func main(){
book := Book(Title: "shu1", auth: "zhang3")
}
给函数可以传结构体指针,但是函数内部还是正常通过 . 访问成员
继承
type SuperBook struct{
Book //SuperBook类继承了上面Book类的方法
level int
}
// 可以重定义方法
//
func main(){
//两种构造
s := SuperBook(Book{"name","auth"}, 18)
var s2 Book
s2.name = "name2"
}
interface 多态
“只要你实现了这些方法,你就是这个类型。”
interface 只能定义方法集,不能拥有成员变量。
本质:
// Go 的 interface 是一个结构体,底层表示可以抽象为两个字段:
type iface struct {
tab *itab // 指向方法表(type + 实现)
data unsafe.Pointer // 指向实际的数据
}
形式一:接口指针
package main
import "fmt"
//接口 本质是一个指针
type AnimalIF interface{
Sleep()
GetType() string // 获取动物种类
}
//【只有把该接口的所有方法都实现的类,才能通过接口指针调用对应的类方法】
type Cat struct{
type Color
}
func (this *Cat) Sleep(){
fmt.Println("Cat is sleeping")
}
func (this *Cat) GetType() string{
return "Cat"
}
//多态
func main(){
var animal AnimalIF //父类指针 接口数据类型
animal = &Cat{"Green"}
animal.Sleep() //调用的就是Cat的Sleep()方法
}
形式二:函数传递接口指针参数
// 同上类
func showAnimal(animal AnimalIF){
animal.Sleep()
fmt.Println("type = ", animal.GetType()) //直接调用【具体对象】的方法了
}
interface 空接口可作万能类型:
上图代码:
package main
import "fmt"
func myFunc(arg interface{}) {
fmt.Println("myFunc is calling:", arg)
}
type Book struct {
auth string
}
func main() {
book := Book{"zhang3"}
myFunc(book)
myFunc(1234)
myFunc("go学长")
}
interface类型的空接口 断言:
(必须是空接口) 原理是反射
arg.(<类型>)
机制
调度器
Go 的调度器(Scheduler)是其运行时系统最核心的组件之一,它决定了 Go 协程(goroutine)如何被调度和运行在操作系统线程(OS thread)之上。Go 的高并发能力,核心就在于它这个 “M:N 调度器” 的高效实现。
GPM:
M:Machine,操作系统线程(OS thread)
P:Processor,执行 goroutine 的上下文资源,维护任务队列。
G:Goroutine,轻量级协程,即你的 Go 函数
M:
Go 运行时有一个 M 的空闲池(runtime.mcache),执行完 G 的 M 会归还,不会频繁创建销毁。
一般来说,一个 Go 程序会根据 并发情况、GOMAXPROCS 设置、syscall 阻塞等情况,创建 数个到几十个 M(线程)。在高并发、IO 密集程序中,可能会增长到上百个甚至更多。P:
P 维护了队列、内存、定时器等所有支撑 goroutine 正常运行的资源,这样线程切换不会带来大量资源迁移。
P 维护自己的 goroutine 队列 - runq (通常为 256 个 G)。新的 goroutine 创建后,优先放入当前 P 的 runq;如果本地 runq 满了,会将一半 goroutine 转移到全局队列。G:
调度过程:
go func()
创建一个G,加入当前 上下文P 的本地任务队列- 每个 上下文P 会绑定一个 线程M,M从P的任务队列中取G执行。
- 抢占机制:Go 运行时调度器会定期抢占长时间运行的 G,防止“霸占 CPU”。
- 均衡调度算法:工作窃取(work stealing):如果某个 P 的 G 队列空了,它会从其他 P 那里“偷”任务。
- 系统调用的处理:如果某个 G 发起了 syscall(如文件读写、网络 IO),其所在的 M 会被释放,P 绑定新的 M 来继续执行别的 G(防止阻塞)。
M:N 调度器(多对多调度器)
M 个用户级线程(协程 G)调度到 N 个内核线程(OS 线程 M)上运行。
让成千上万个轻量任务(G)能够灵活、高效地运行在少量线程(M)上,由 P 统一协调,让开发者无需关心线程切换、调度和资源浪费问题。
runtime
在 Go 中,遇到系统调用(如 I/O 操作)时,会进入 runtime,由 runtime 进行管理和调度处理
如网络IO:
- G 发起 syscall(如 read(fd, buf))
runtime.entersyscall()
do actual syscall
runtime.exitsyscall()
- runtime 标记该 G 为 syscall 状态
当前的 M 被认为“没有 G 在运行”,所以可以解绑 P;P 立刻找别的 G 调度运行,不阻塞。 - syscall 返回后,runtime 恢复调度
G标记恢复,重新被放入 P 的 runqueue,等待调度执行
全局G队列
Go 的 runtime 中,除了每个 Processor(P)拥有自己的本地 run queue(局部 G 队列),还有一个全局 G 队列(global run queue),用于整个调度器协调 goroutine 的执行负载。
- 一些早期创建的 G 没有绑定具体的 P,只能先挂在全局队列;
- 某个 P 的 runqueue 空时可以从全局队列偷 G;
- runtime 会将 G 放回全局队列,避免调度干扰;系统后台线程唤醒的 G,会加入全局队列;
runtime/proc.go
type schedt struct {
...
runqhead uint32
runqtail uint32
runq [gQueueSize]*g
...
}
var sched schedt
反射
reflect 包
变量的结构 pair:
获取 Type 和 Value (字段必须导出(大写首字母),无论是否在同一个包内。Go 的 reflect 包是语言级别的 API,它不区分“是否同包”)
t := reflect.TypeOf(obj) //返回一个表示 obj 类型的 reflect.Type 对象。
v := reflect.ValueOf(obj) //返回一个表示 obj 值的 reflect.Value 对象。
field := v.FieldByName("Name") // 尝试通过字段名 "Name" 查找并返回对应的 reflect.Value。
fmt.Println(field.Interface())
channel
c := make(chan int ,3) // 创建一个缓冲区为 3 的 channel
c <- 1 // 发送,不阻塞(因有缓冲)
x := <-c // 接收,若为空会阻塞
close(c) // 关闭 channel 后,可以读不可以写。
for range 读取 channel:
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
for val := range ch { // ❗若不关闭 channel,range 会一直阻塞等待新数据。
fmt.Println(val)
}
// 输出:1, 2, 3
select 同时监听多个 channel,每个 case 是一个 channel 操作。
select {
case v := <-c1:
fmt.Println("from c1", v)
case c2 <- 100: // 满足条件:c2 有缓冲且还有剩余空间 或者 无缓冲并有 goroutine 正在读
fmt.Println("send to c2")
default:
fmt.Println("no case ready")
}
nil channel 的读写操作都会永久阻塞!
依赖
C:\Users\Administrator>go env
set GO111MODULE=on
set GOARCH=amd64
set GOBIN=
set GOCACHE=C:\Users\Administrator\AppData\Local\go-build
set GOENV=C:\Users\Administrator\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFLAGS=
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMODCACHE=C:\Users\Administrator\go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=C:\Users\Administrator\go
set GOPRIVATE=
set GOPROXY=https://goproxy.io,direct
set GOROOT=F:\2024\my_homework\gwsj\go
set GOSUMDB=sum.golang.org
set GOTMPDIR=
set GOTOOLCHAIN=auto
set GOTOOLDIR=F:\2024\my_homework\gwsj\go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.23.1
set GODEBUG=
set GOTELEMETRY=on
set GOTELEMETRYDIR=C:\Users\Administrator\AppData\Roaming\go\telemetry
set GCCGO=gccgo
set GOAMD64=v1
set AR=ar
set CC=gcc
set CXX=g++
set CGO_ENABLED=1
set GOMOD=NUL
set GOWORK=
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set PKG_CONFIG=pkg-config
set GOGCCFLAGS=-m64 -mthreads -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=C:\Users\ADMINI~1\AppData\Local\Temp\go-build3313090824=/tmp/go-build -gno-record-gcc-switches
GOPROXY
指定项目第三方依赖库的下载地址
GOSUMDB 会在 拉取模块时校验,防篡改
用 GOPRIVATE 指定为私有仓库,不会进行GOPROXY下载校验
GOPATH
在 Go1.11 之前,Go 没有“官方”的依赖管理系统,社区一开始用 GOPATH 方式,所有项目必须在 GOPATH,而且无法指定版本,永远下载最新版
GOModules
C++需要自己去下载,指明库的路径 (C++ 没有“官方的行李打包规范”,只能自己想办法。)
但是Go Modules 只要说清楚,系统自动做。
Go Modules 就是 Go 的“包管理工具”。
C:\Users\Administrator>go mod
Go mod provides access to operations on modules.
..
工作流程:
go mod init myproject
这会生成一个 go.mod 文件,像身份证一样告诉 Go:这是一个模块项目。- 写代码时用了别人的包:import “github.com/gin-gonic/gin”
只需go get
github.com/gin-gonic/gin@v1.9.0,Go就会自动去下载,更新 go.mod、go.sum,并把下载的代码都缓存到 $GOPATH/pkg/modgo.mod
:记录你用了哪些包、用了哪个版本。go.sum
:记录每个包的校验值,保证别人能下载到 完全一致 的版本。
Go Modules 会自动处理依赖树。比如你用了 A,A 又用了 B 和 C,Go 会自动把 B 和 C 也拉下来,并确保不会冲突。
erlang?
Erlang 是军用级飞船,出了故障能自动重启继续飞;
Go 是工程师造的卡车,好开,但出了问题你得下车修。