Go 代码结构

与 Rust 不同的是,Go 将代码划分为包。一个 Go 语言必须在每个源文件的头部显式写明所属包名。另一点是 Go 对代码的格式进行了严格要求,例如大括号不能换行等

Go 会在语句的末尾自动添加分号。因此一般无须手动添加。但是如果希望在同一行写多个语句,那么需要手动添加分号

Go 语言的注释与 C++ 相同,分为行注释和块注释,且写法相同。

基础语法

Go 语言提供了布尔、浮点数、复数、字符串、指针、数组、结构等类型。并且使用了后置类型声明。

另外还提供了一个 nil 值/类型

以下几种类型为 nil:

var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error 是接口

Go 语言 声明 变量有以下几种方式:

var a = 10
a := 10
var a, b, c = 1, 2, 3
var a, b, c int32 = 1, 2, 3
// 下面的写法一般用于声明全局变量
var (
   A int64
   B string
)

局部变量声明后必须要使用,否则产生编译错误。全局变量则不然

声明常量使用 const 修饰

数组的声明方式为:

a := []int{1, 2, 3, 4, 5}
for _, it := range a {
    fmt.Println(it)
}

数组的大小可以手动指定,也可以直接忽略

Go 语言中的空指针被称为 nil,统一用来代表空值

另外介绍一个关键字 iota

iota 用于 const 表达式中,让编译期自动计算它的值,计算方式如下:

  • 从第一个 const 表达式开始计算,初值为 0

  • 每次碰到一个 const 变量加一

  • 没有显式初始化的变量将被编译期赋值

例如:

const (
   a = 2
   b = iota // b = 1
   c        // c = 3
   d = "yes"
   e // e = "yes"
   k = iota // 5
   j // j = 6
)

从中也可以看出这种初始化的特点:未显式初始化的变量会延续上一个变量的类型和值。多次使用 itoa 不会更改编译期的计数

Go 语言中支持的运算符与 C++ 完全相同

引用类型

Go 语言中单字数据使用拷贝语义,多字数据使用引用语义

条件语句

条件语句与 Rust 一样:

a := true
if a {
    fmt.Println("true")
}

循环语句

Go 语言中的循环语句如下:

// 一般格式
 for i := 0; i < 10; i++ {
     fmt.Println(i)
 }
 i := 1
// 类 while 形式
 for i < 10 {
     fmt.Println(i)
     i++
 }
// for true{}
 for {
     break
 }
 str := "hello"
// 展开 dict, list
 for index, ch := range str {
     fmt.Printf("%d, %c\n",index , ch)
 }

函数

Go 语言中的函数也采用了后置类型声明,但是和 Rust 不同的是不再需要箭头:

func add(a, b int) int {
   return a + b
}

func main() {
   fmt.Println(add(1, 2))
}

还可以返回多个值

func swap(x, y int) (int, int) {
   return y, x
}

Go 语言中的参数默认是值传递,如果需要引用传递,则需要使用指针:

func plus(a *int) int {
   *a++
   return *a
}

func main() {
   a := 10
   fmt.Println(plus(&a))
}

结构体

type Student struct {
   id   string
   name string
   age  uint16
}

func main() {
   stu1 := Student{id: "12345", name: "小明", age: 16}
   fmt.Println(stu1.name)

   stuPtr := &stu1
   fmt.Println(stuPtr.age)
}

从中也可以看出来 Go 语言中的指针语法声明的实际上是类似于引用的变量

切片

Go 语言切片的方式和其它语言相同:

arr := []int{1, 2, 3, 4, 5}
slice1 := arr[1:]
slice2 := arr[:2]
slice3 := arr[1:2]
fmt.Println("slice1")
for _, it := range slice1 {
    fmt.Println(it)
}
fmt.Println("slice2")
for _, it := range slice2 {
    fmt.Println(it)
}
fmt.Println("slice3")
for _, it := range slice3 {
    fmt.Println(it)
}

defer

关键字 defer 用来将一个语句推迟到函数结束时调用。并遵循以下规则:

  • 函数的调用顺序与 defer 声明的顺序相反。也就是说先声明的被后调用

  • defer 执行的函数的参数在声明的时候被固定。也就是说参数的传递是拷贝而不是引用

func main(){
    if true {
        defer fmt.Println("defer")
    }
    fmt.Println("main body")
}

输出结果为 :

main body
defer

字典

字典的使用方式为:

map1 := map[string]string{}
map1["a"] = "b"
map1["c"] = "d"

for k, v := range map1 {
     fmt.Println(k, v)
 }

字典底层使用的是 hash 表,因此是无序的

面向对象

Go 语言和 Rust 类似,将数据和方法进行分离,函数的实现是面向接口的。

type Student struct {
   id   string
   name string
   age  int16
}

type show interface {
   output()
}

func (stu Student) output() {
   fmt.Println(stu.id, stu.name, stu.age)
}

func main() {
   stu := Student{"12345", "小明", 16}
   stu.output()
}

可以看到,实际上和 Rust 走的路子相同。可以理解为特化接口。只是 Rust 从代码中可以理解出来,而 Go 相对隐晦

另外,和结构体绑定的函数称为方法,方法会将结构体按指针或值传递并绑定为参数:

type Stu struct {
    id int
}

func (t Stu)setId(){
    t.id = 20
}

func (t *Stu)setPid(){
    t.id = 30
}

如代码所示,setId 传入的结构体为按值传入,setPid 传入的结构体则是按指针传入。第一个方法自然没办法实现想要的语义。

如果使用值类型调用 setPid,那么传入的实际上是值的一个副本(毕竟右值是没办法取地址的)

匿名字段

如代码所示:

type User struct {
    id   int
    name string
}

type Manager struct {
    User
}

这样,Manager 就获得了 User 的字段。这就是 Go 中的继承。自然,Manager 的同名方法也会覆盖 User 的同名方法

接口

Go 中的接口 是类型的一种 。接口用来保证实现此接口的结构体必定实现了此接口的函数。

接口同样可以被继承。另外,与其他语言中显式实现接口不同,Go 中 实现了接口中的所有方法也就实现了接口 。例如:

type Anmial interface {
   say()
}

type Dog struct {
   name string
}

func (self Dog) say() {
   fmt.Println(self.name)
}

func main() {
   var xab Anmial = Dog{}
   dog := Dog{}
   dog.name = "Dog"
   xab.say()
}

如代码所示,只要实现了接口需要的函数,那么就实现了接口

错误处理

错误被声明为一个接口,然后通过返回值返回出去

func Sqrt(f float64) (float64, error) {
   if f < 0 {
      return 0, errors.New("error")
   } else {
      return math.Sqrt(f), errors.New("")
   }
}

func main() {
   if _, err := Sqrt(-1); err.Error() == "error" {
      fmt.Println("error")
   }
}

定义一个接口的方式只是简单地实现 Error 接口:

type ParameterError struct {
   Message string
}

func (prarm *ParameterError) Error() string {
   return prarm.Message
}

类型断言

类型断言可以当作强制类型转换,用来判断值是否能转换到目的类型。其格式为:

value, ok := x.(T)

当 x 可以转为类型 T 时,ok 不为 nil,且由 value 储存结果值。一个常见的用法是:

if exitError, ok := err.(*exec.ExitError); ok {
   os.Exit(exitError.ExitCode())
} else {
   panic(err)
}

协程

协程的开启非常简单,只需要在函数或者代码块前面添加 go 关键字即可:

func output(str string) {
   for i := 10; i > 0; i-- {
      time.Sleep(100 * time.Millisecond)
      fmt.Println(str)
   }
}

func main() {
   go output("hello")
   output("world")
}

注意:如果主线程结束的太快,协程可能由于得不到调度而退出(外观和线程一样),为了解决这个问题,可以使用 WaitGroup:

import (
    "sync"
)

var waitGroup sync.WaitGroup

func func1(){
    defer waitGroup.Done()
}

func main(){
    waitGroup.Add(1)
    go func1()
    waitGroup.Wait()
}

WaitGroup.Add 设置了需要等待的协程数量,而 WaitGroup.Done() 会将该值减小 1。当减小为零时,WaitGroup.Wait() 退出

通道

通道用来在两个协程之间传递数据,通道有两种定义方式:

ch1 := make(chan int) // 声名一个无限容量的通道
ch2 := make(chan int, 3) // 声名一个具有三个容量缓存的通道
  • 写一个已满的通道会导致写端被阻塞。

  • 读无数据的通道,读端就会被阻塞。

如果一个通道在运行时没有了读端,那么此通道会被自动 close,再进行写就会触发 panic

此外,通道还可以被关闭或者置为 nil :

ch1 := make(chan bool)
close(ch1)

ch2 := make(chan bool)
ch2 = nil
  • 对已经关闭的通道进行写会触发 panic

  • 对已经关闭的通道进行读会返回相关类型的零值

  • 对值为 nil 的通道进行读写会触发 panic

  • 如果通道被置为 nil,则不会被 select 选中

如下形式的通道调用会导致 ok 设置为 false:

x, ok := <-c

一个无缓存的使用例子为:

func sum(arr []int, c chan int) {
   sum := 0
   for _, v := range arr {
      sum += v
   }
   c <- sum

}

func main() {
   arr := []int{1, 2, 3, 4, 5}
   c := make(chan int)
   go sum(arr[:3], c)
   go sum(arr[3:], c)
   x, y := <-c, <-c
   fmt.Println(x, y)
}

调试

Go 程序本身可以被 gdb 调试,但是默认的构建不利于调试。要想启用完整的 gdb 支持,构建时需要使用 :

$ go build -gcflags "-N -l"

主要是禁用了优化

在发布时构建则为 :

$ go build -ldflags "-s -w"

构建时初始化变量

Go 可以在构建时通过添加 -ldflags -X "importpath.varname=value" 的形式初始化变量。例如:

package main

import "fmt"

var (
    version   string
    buildTime string
    osArch    string
)

func main() {
    fmt.Printf("Version: %s\nBuilt: %s\nOS/Arch: %s\n", version, buildTime, osArch)
}

然后执行 :

$ go run  -ldflags "-X 'main.version=0.1' -X 'main.buildTime=2022-03-25' -X 'main.osArch=darwin/amd64'" main.go

输出为 :

Version: 0.1
Built: 2022-03-25
OS/Arch: darwin/amd64

导出变量和函数

Go 使用约定来保证变量和函数的可见性。

只有首字母为大写的函数和变量才能为外部所看到。

例如:

type Student struct{
    ID int // 外部可见
    name string // 外部不可见
}

stu := Student{
   ID: 10, // 正确
   name : "xiaoming" // 错误
}

如果尝试初始化 name 变量,则会报错 :

implicit assignment to unexported field message in xxx literal compiler (UnexportedLitField)

与结构体类似,包中的变量和函数也是以类似的方式导出的

调用私有方法和成员

调用私有方法的方式是使用 go:linkname 注释:

//go:linkname setResources github.com/containerd/cgroups/v2.setResources
func setResources(path string, resources *v2.Resources) error

调用私有成员的方式是使用 unsafe

type Manager struct { // v2.Manager for access private path
   _    string
   path string
}

p := *(*Manager)(unsafe.Pointer(manager))
if err := setResources(p.path, &v.Resources); err != nil {
   if err := manager.Delete(); err != nil {
      return err
   }

   return err
}

Go Mod

使用指定版本的分支 :

github.com/containerd/cgroups main

然后直接 go build ,会自动更改为 hash :

github.com/containerd/cgroups v1.0.4-0.20220317195426-f8328fdc061d

泛型

若一个类型指定了[类型参数],则此类型成为泛型类型。

type List[T any] struct {
	next  *List[T]
	value T
}

类型参数

类型参数的语法如下:

TypeParameters  = "[" TypeParamList [ "," ] "]" .
TypeParamList   = TypeParamDecl { "," TypeParamDecl } .
TypeParamDecl   = IdentifierList TypeConstraint .

其中 TypeConstraint 即为:

TypeConstraint
TypeElem       = TypeTerm { "|" TypeTerm } .
TypeTerm       = Type | UnderlyingType .
UnderlyingType = "~" Type .

类型约束

泛型中可以使用类型约束简化对 T 类型的描述,语法为:

type Number interface {
	int | float32
}

这等价于:

func Add[T int | float32](a T, b T) T {
	return a + b

}

类型断言

可以通过类型断言来获取模板的具体类型:

func Add[T int | float32](a T, b T) T {
	switch any(a).(type) {
	case int:
		println("int")
	case float32:
		println("float")
	}
	return a + b

}

内存管理

除了使用生命的方式创建对象外,Go 还可以提供了 make 来创建 slice, map 或者 chan、提供了 new 来分配内存。new 分配的内存是初始化为零的,类似于 calloc。

尽管 new 返回的是指针,但是并不意味着类型是分配在堆上的。

Go 会尽可能将对象分配在栈上,但是以下情况除外:

  • 变量太大。

  • 变量被取地址且发生了逃逸。

然而,若被分配的内存大小为 0。即使变量通过了逃逸分析,依然不会发生内存分配。这是因为 mallocgc 中会对分配的大小进行检查:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    // ...
	if size == 0 {
		return unsafe.Pointer(&zerobase)
	}
    // ...
}
Last moify: 2025-01-17 02:16:27
Build time:2025-07-18 09:41:42
Powered By asphinx