Go 基础

Go 特点:简单,高效,并发友好。

安装

Go 官网:https://golang.google.cn/

安装方法参考:Download and install

# 先从官网下载 Go
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.25.5.linux-amd64.tar.gz

# 设置环境变量
export PATH=$PATH:/usr/local/go/bin

# 查看版本
go version

学习资料

Golang 中文学习文档:https://golang.halfiisland.com/

工程创建

# 创建项目目录
mkdir mydata

cd mydata

# 初始化项目
go mod init mydata

# 创建 main.go 文件
touch main.go

# 编写代码
vim main.go

示例代码:

package main   // 包名
import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

清理 go.mod 中的无用依赖项:

go mod tidy

编译:

go build main.go

运行:

go run main.go

Go 基础

在 Go 中,程序是通过将包链接在一起来构建的。Go 中进行导入的最基本单位是一个包,而不是 .go 文件。包其实就是一个文件夹,英文名 package,包内共享所有变量、常量,以及所有定义的类型。包的命名风格建议都是小写字母,并且要尽量简短。package 关键字声明了当前 go 文件属于哪一个包。

cmd    # cmd 包,文件夹名即为包名
--> a.go   # package cmd
--> b.go   # package cmd

包内共享所有变量、常量,以及所有定义的类型,但对于包外而言并不是这样,有时候你并不想让别人访问某一个类型,所以就需要控制可见性。包的可见性规则:

  • 名称大写字母开头,即为公有类型/变量/常量
  • 名字小写或下划线开头,即为私有类型/变量/常量

导入一个包来使用这个包中的类型/方法/函数/变量,导入的语法就是 import 加上包名:

import "fmt"

Go 中约定,一个包内名为 internal 包为内部包,外部包将无法访问内部包中的任何内容,否则的话编译不通过。

函数

函数声明:

func 函数名([参数列表]) [返回值] {
  函数体
}

变量声明,var 变量名 类型名

var intNum int

指针:Go 保留了指针,在一定程度上保证了性能,同时为了更好的 GC 和安全考虑,又限制了指针的使用。关于指针有两个常用的操作符,一个是取地址符 &,另一个是解引用符 *

延迟调用:defer 关键字可以使得一个函数延迟一段时间调用,在函数返回之前这些 defer 描述的函数最后都会被逐个执行。

func main() {
  Do()
}

func Do() {
  defer func() {
    fmt.Println("1")   // 最后执行
  }()
  fmt.Println("2")   // 先执行
}

init 函数是一个特殊的初始化函数,用于在程序启动时执行包级别的初始化逻辑。它无需手动调用,由 Go 运行时自动执行。

并发

Go 语言对于并发的支持是纯天然的,这是这门语言的核心所在,其上手难度相对较小,开发人员不太需要关注底层实现就能做出一个相当不错的并发应用,提高了开发人员的下限。

协程:协程(coroutine)是一种轻量级的线程,或者说是用户态的线程,不受操作系统直接调度,由 Go 语言自身的调度器进行运行时调度,因此上下文切换开销非常小,这也是为什么 Go 的并发性能很不错的原因之一。

在 Go 中,创建一个协程十分的简单,仅需要一个 go 关键字,就能够快速开启一个协程,go 关键字后面必须是一个函数调用。

func main() {
  go fmt.Println("hello world!")
  go hello()
  go func() {
    fmt.Println("hello world!")
  }()
}

func hello() {
  fmt.Println("hello world!")
}

Go 提供了非常多的并发控制手段,常用的并发控制方法有三种:

  • channel:管道
  • WaitGroup:信号量
  • Context:上下文 三种方法有着不同的适用情况,WaitGroup 可以动态的控制一组指定数量的协程,Context 更适合子孙协程嵌套层级更深的情况,管道更适合协程间通信。对于较为传统的锁控制,Go 也对此提供了支持:
  • Mutex:互斥锁
  • RWMutex:读写互斥锁

channel:管道,即通过消息来进行内存共享,channel 就是为此而生,它是一种在协程间通信的解决方案。

在创建管道时,有且只有一种方法,那就是使用内置函数 make,对于管道而言,make 函数接收两个参数,第一个是管道的类型,第二个是可选参数为管道的缓冲大小。例子如下:

intCh := make(chan int)
// 缓冲区大小为 1 的管道
strCh := make(chan string, 1)

在使用完一个管道后一定要记得关闭该管道,使用内置函数 close 来关闭一个管道,该函数签名如下。

func close(c chan<- Type)

func main() {
  intCh := make(chan int)    // 创建管道
  // do something
  close(intCh)  // 关闭管道
}

对于一个管道而言,Go 使用了两种很形象的操作符来表示读写操作:

ch <- 数据  // 表示对一个管道写入数据
变量 := <-ch  // 表示对一个管道读取数据

示例:

func main() {
  // 创建无缓冲管道
  ch := make(chan int)
  defer close(ch)
  go func() {
    // 写入数据
    ch <- 123
  }()
  // 读取数据
  n := <-ch
  fmt.Println(n)
}

管道中的数据流动方式与队列一样,即先进先出(FIFO),协程对于管道的操作是同步的,在某一个时刻,只有一个协程能够对其写入数据,同时也只有一个协程能够读取管道中的数据。


参考书: Go 语言设计与实现 Go 浅析