Go 语言基础语言 | 青训营笔记

105 阅读14分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

1. 简介

Go 语言保证了既能到达静态编译语言的安全和性能,又达到了动态语言开发维护的高效率。 使用一个表达式来形容 Go 语言:Go = C + Python,Go 语言既有 C 静态语言程序的运行速度,又能达到 Python 动态语言的快速开发。

Go 发展历史

  • 2007 年,谷歌工程师 Rob Pike、Ken Thompson、Robert Griesemer 开始设计一门全新的语言,这是Go语言的最初原型。
  • 2009 年 11 月 10 日,Google 将 Go 语言以开放源代码的方式向全球发布。
  • 2015 年 8 月 19 日,Go 1.5 版发布,本次更新中移除了 “最后残余的 C 代码”。
  • 2017 年 2 月 17 日,Go 语言 Go 1.8 版发布。
  • 2017 年 8 月 24 日,Go 语言 Go 1.9 版发布。
  • 2018 年 2 月 16 日,Go 语言 Go 1.10 版发布。

1.1. SDK

  1. 安装 Go SDK 并配置环境变量

    • 下载地址:golang.google.cn/dl/
    • 环境变量(可选):
      GOROOT = C:\Program Files\DevTools\go1.17.8.windows-amd64
      PATH = %GOROOT%\bin
      GOPATH = D:\go
      
  2. 配置:

    # 查看版本
    PS > go version
    go version go1.19.3 windows/amd64
    
    # 查看 Go 相关的环境变量
    PS > go env
    set GOVERSION=go1.19.3
    set GOBIN=
    set GOROOT=D:\Program Files\go
    set GOPATH=C:\Users\aBadString\go
    
    set GO111MODULE=
    set GOPROXY=https://proxy.golang.org,direct
    

    需要设置以下配置(goproxy.cn)

    go env -w GO111MODULE=on
    go env -w GOPROXY=https://goproxy.cn,direct
    
  3. 创建并编辑源文件 hello.go

    package main
    
    import "fmt"
    
    func main() {
        fmt.Println("hello, go")
    }
    
  4. 编译运行

    PS > go build hello.go
    PS > .\hello.exe
    hello, go
    

    或者

    PS > go run hello.go
    hello, go
    
  • Go 源代码文件以 .go 结尾。
  • package main 每个 Go 源代码文件必须归属于一个包中。
  • func main() {} 主函数,main 函数所在包必须是 main
  • Go 的语句后面无需添加分号,Go 会在每行结尾自动添加分号。不要把多条语句放一行。
  • Go 中声明却未使用的变量和导入却未使用的包会报错。

1.2. 编译与运行

  • Go 的 Runtime 负责内存管理、垃圾回收、协程调度等工作。
  • 编译时,Runtime 是和程序员写的源代码一个编译、打包为可执行程序。

Go 程序生成过程

  1. 词法分析:源代码文本转为 Token 序列(Token 是代码中的最小寓意结构)
  2. 句法分析:Token 序列转为语法树 AST
  3. 语义分析:类型检查、类型推断、函数内联、逃逸分析
  4. 中间码生成:为了处理不同平台的差异,先生成中间代码 SSA
  5. 代码优化
  6. 机器码生成:先生成 Plan9 汇编代码,最终编译为机器码(hello.a 文件)。
  7. 链接

Go 程序的入口

Go 程序的入口是 GOROOT\src\runtime\rt0_xxx_yyy.sxxx 表示操作系统、yyy 表示芯片平台,例如 rt0_linux_amd64.s
这是一个汇编语言的源代码。

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#include "textflag.h"

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
    JMP    _rt0_amd64(SB)

TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
    JMP    _rt0_amd64_lib(SB)
// _rt0_amd64 is common startup code for most amd64 systems when using
// internal linking. This is the entry point for the program from the
// kernel for an ordinary -buildmode=exe program. The stack holds the
// number of arguments and the C-style argv.
TEXT _rt0_amd64(SB),NOSPLIT,$-8
    MOVQ    0(SP), DI    // argc
    LEAQ    8(SP), SI    // argv
    JMP     runtime·rt0_go(SB)
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
    // 1. 读取命令行参数
    // copy arguments forward on an even stack
    MOVQ    DI, AX        // argc
    MOVQ    SI, BX        // argv
    SUBQ    $(5*8), SP        // 3args 2auto
    ANDQ    $~15, SP
    MOVQ    AX, 24(SP)
    MOVQ    BX, 32(SP)

    // 2. 初始化 g0 的执行栈
    // create istack out of the given (operating system) stack.
    // _cgo_init may update stackguard.
    MOVQ    $runtime·g0(SB), DI
    LEAQ    (-64*1024+104)(SP), BX
    MOVQ    BX, g_stackguard0(DI)
    MOVQ    BX, g_stackguard1(DI)
    MOVQ    BX, (g_stack+stack_lo)(DI)
    MOVQ    SP, (g_stack+stack_hi)(DI)

    // ...
    // ...


    // 3. 运行时检查
    CALL    runtime·check(SB)

    // 4. 参数初始化
    MOVL    24(SP), AX        // copy argc
    MOVL    AX, 0(SP)
    MOVQ    32(SP), AX        // copy argv
    MOVQ    AX, 8(SP)
    CALL    runtime·args(SB)

    CALL    runtime·osinit(SB)
    // 5. 调度器初始化
    CALL    runtime·schedinit(SB)

    // 6. 创建主协程
    // create a new goroutine to start program
    MOVQ    $runtime·mainPC(SB), AX        // entry
    PUSHQ    AX
    // 6.1. 创建一个新的协程来执行 `runtime.main`
    CALL    runtime·newproc(SB)
    POPQ    AX

    // 6.2. 初始化一个 M 用来调度主协程
    // start this M
    CALL    runtime·mstart(SB)

    CALL    runtime·abort(SB)    // mstart should never return
    RET

// ...
// ...

// mainPC is a function value for runtime.main, to be passed to newproc.
// The reference to runtime.main is made via ABIInternal, since the
// actual function (not the ABI0 wrapper) is needed by newproc.
DATA    runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)
GLOBL    runtime·mainPC(SB),RODATA,$8
  1. 读取命令行参数:将 argcargv 复制到栈上。
  2. 初始化 g0 的执行栈:g0 是用来调度协程的而产生的协程,是 Go 程序的第一个协程。
  3. 运行时检查:调用 Go 函数 runtime.check,检查主要内容是各种类型的长度、结构体字段偏移、CAS 操作、指针操作、atomic 原子操作、栈大小是否是 2 的幂次。
  4. 参数初始化runtime.args,将命令行参数数量赋值给 argc int32,参数值赋值给 agrv **byte
  5. 调度器初始化runtime.schedinit,主要内容有全局栈空间内存分配、堆内存空间的初始化、初始化当前系统线程、算法初始化(map、hash)、加载命令行参数到 os.Args、加载操作系统环境变量、垃圾回收器的参数初始化、设置 process 数量。
  6. 创建主协程:创建一个新的协程来执行 runtime.main,将该协程放入调度器等待;初始化一个 M 用来调度主协程;主协程被调度到时将执行主函数 runtime.main(即 GOROOT\src\runtime\proc.go 文件中的 main 函数)。
  7. 主协程执行主函数 runtime.main
    1. 执行 runtime 包中的 init 方法
    2. 启动 GC
    3. 执行用户包中的 init 方法
    4. 执行用户的主函数 main 包中的 main 方法
// GOROOT\src\runtime\proc.go

// The main goroutine.
func main() {
    // ...
    
    doInit(&runtime_inittask) // Must be before defer.

    // ...

    gcenable() // 开启 GC

    // ...

    // 程序库文件不会执行 main 函数
    if isarchive || islibrary {
        // A program compiled with -buildmode=c-archive or c-shared
        // has a main, but it is not executed.
        return
    }

    // !!! 这两行是重点: 调用了函数 main_main
    fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
    fn()
    // !!! 这两行是重点: 调用了函数 main_main

    if raceenabled {
        racefini()
    }
}


//go:linkname main_main main.main
func main_main()
// !!! go:linkname 引导编译器将当前方法或变量在编译时链接到指定的位置的方法或变量
// 所以实际上调用的是 main 包下的 main 函数, 即我们写的 main 函数

1.3. 依赖管理

1.3.1. GOPATH

  • 环境变量 GOPATH 默认为 ~/go,可以通过修改环境变量 GOPATH 来修改此目录。
  • 所有项目的源代码都必须放在 $GOPATH/src
  • 第三方库依赖也以源码的形式放在 $GOPATH/src 下,若两个项目依赖同一个三方库的不同版本则无法做到,需要使用 GOVENDOR。
$GOPATH/src/
    |-- project2
    |-- project2
    |-- ...
    |-- library1
    |-- library2
    |-- ...

1.3.2. GOVENDOR

在项目目录下建立一个 vendor 目录,里面存放当前项目依赖的第三方库源代码版本

$GOPATH/src/
    |-- project2
        |-- vendor
            |-- library1
    |-- project2
        |-- vendor
            |-- library1

在执行 go build 或 go run 命令时,会按照以下顺序去查找包:

  1. 当前包下的 vendor 目录
  2. 向上级目录查找,直到找到 src 下的 vendor 目录
  3. GOROOT/src
  4. GOPATH/src

1.3.3. GO MOD (推荐)

  • 使用 GO MOD 依赖管理方式时,项目源代码可以放在任意位置。
  • go get XXX 命令来拉取依赖库
  • 第三方库依赖放在 $GOPATH/pkg/mod/库名@版本
  • go.mod 文件记录了当前项目所依赖的库

Go 1.13 以上版本,设置以下配置(goproxy.cn)

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
  • 每个包(库)视为一个 Git 项目的源码,GO MOD 将包和 Git 项目关联起来
  • 包的版本则用 Git Tag 来表示

1.4. goimports

需要在任意一个 go mod 项目下执行命令:

go get golang.org/x/tools/cmd/goimports
go install golang.org/x/tools/cmd/goimports

2. 基础

2.1. 变量

变量三要素:变量名、值、数据类型

变量的声明

// var 变量名 变量类型 = 初始值
var a int = 3

var b int // 默认值 0
var c = 2 // 自动类型推导

d := 6 // 只可用在函数内, 且 d 在这之前未声明过

声明的变量会被默认赋值:
整型和浮点型变量的默认值为 00.0。 复数类型的默认值为 0 + 0i。 字符串变量的默认值为空字符串 ""。 布尔型变量默认为 false。 切片、函数、指针变量的默认为 nil

多变量声明

var n1, n2, n3 int
var n4, n5, n6 int = 1, 2, 3

// 多个变量类型不一样
var (
    e int
    f string
)

var (
    m = 1
    n = '0'
)

var c1, c2, c3 = true, '1', "str"


g, h := 'g', 19.8

_, i := 34, 35  // _ 是一个特殊变量, 任何赋给它的值都被丢弃
_, j := 34, 35  // 可以多次声明 _

2.2. 常量

常量在编译时被创建并计算,只能是数字、字符串或布尔值。

const a = 6

const (
    b = iota
    c = iota
)

fmt.Println(a, b, c)
// 6 0 1

第一个 iota 表示为 0,当 iota 再次在新的一行使用时,它的值增加了 1。

const (
    b = iota
    c
)
fmt.Println(b, c)
// 0 1

后面的 iota 可以被省略,c 被隐式复制为 iota

const (
    b = iota
    _
    c
)
fmt.Println(b, c)
// 0 2

_ 可以略过一次 iota。

用常量和 iota 可以达到枚举的效果

iota 还可用于表达式中:

const (
    b int64 = 1 << (10 * iota)
    kb
    mb
    gb
)
fmt.Println(b, kb, mb, gb)
// 1 1024 1048576 1073741824
// b  int64 = 1 << (10 * 0)
// kb int64 = 1 << (10 * 1)
// mb int64 = 1 << (10 * 2)
// gb int64 = 1 << (10 * 3)

2.3. 标识符

  1. 以大小写字母或者下划线开头,后接大小写字母、数字、下划线。数字不能做开头。
  2. Go 严格区分大小写
  3. 单独一个下划线 _ 是一个特殊的空标识符。仅被用作占位符,不能作为标识符使用。
  4. 不能使用保留关键字作为标识符。

标识符命名规范

  1. 变量名、函数名、常量名采用驼峰命名法。 若首字母大写则可被其他包访问;首字母小写只能在本包中使用。 Go 中没有访问权限修饰关键字,只能通过标识符的命名来控制访问权限。
  2. 包名全小写,与目录保持一致。不要和标准库冲突。

2.4. 保留关键字(25 个)

break     default      func    interface  select
case      defer        go      map        struct
chan      else         goto    package    switch
const     fallthrough  if      range      type
continue  for          import  return     var

2.5. 预定义标识符(36 个)

内建常量内建类型内建函数
trueintmake
falseint8len
iotaint16cap
nilint32new
int64append
uintcopy
uint8close
uint16delete
uint32complex
uint64real
uintprtimag
float32panic
float64recover
complex64
complex128
bool
byte
rune
string
error

3. 数据类型

3.1. 基本数据类型

  • 基本数据类型
    • 数值型
      • 整数型
        • int、int8、int16、int32、int64
        • uint、uint8、uint16、uint32、uint64
        • unitptr
        • byte(unit8 的别名)

        int、uint 是 32 或 64 位之一,不会定义成其他值

      • 浮点型:float32、float64
      • 复数型:complex64、complex128
    • 字符型
      • 无专门的字符型,使用 byte 来保存单个字母字符,更多字节的使用 int 等
      • rune: int32 的别名,表示一个 Unicode 码点 type rune = int32
    • 布尔型:bool
    • 字符串:string
  • 派生数据类型
    • 指针 pointer
    • 数组
    • 结构体 struct
    • 管道 channel
    • 函数
    • 切片 slice
    • 接口 interface
    • map

值类型:数值、字符、字符串、布尔、数组 引用类型:切片(slice)、字典(map)、函数(func)、方法、管道(chan)

Go 的类型全部都是独立的,并且混合用这些类型向变量赋值会引起编译器错误:

var a int = 5
var b int32 = a + a  // 无法将 'a + a' (类型 int) 用作类型 int32

3.1.1. 整数类型

  1. Go 的整数分为无符号和有符号。
  2. int、uint 大小与平台类型有关,但一定是 32 或 64 位之一,不会定义成其他值。
  3. 整数字面量默认为 int 类型。
    fmt.Printf("%T\n", 1)
    // int
    fmt.Println(reflect.TypeOf(1), unsafe.Sizeof(1))
    // int  8(字节)
    
  4. 整数字面量可以使用下划线分隔各位数字,编译时将自动删除下划线。
// 有符号整数
{
    // 整形字面量默认为 int
    // 64 bit 系统中 int 占 64 bit
    var i = 9_223_372_036_854_775_807
    var i8 int8 = 127
    var i16 int16 = 32_767
    var i32 int32 = 2_147_483_647
    var i64 int64 = 9_223_372_036_854_775_807
    fmt.Println(i, i8, i16, i32, i64)
    // 9223372036854775807 127 32767 2147483647 9223372036854775807
}
{
    var i = -9_223_372_036_854_775_808
    var i8 int8 = -128
    var i16 int16 = -32_768
    var i32 int32 = -2_147_483_648
    var i64 int64 = -9_223_372_036_854_775_808
    fmt.Println(i, i8, i16, i32, i64)
    // -9223372036854775808 -128 -32768 -2147483648 -9223372036854775808
}

// 无符号整数
{
    var ui8Min uint8 = 0
    var ui8Max uint8 = 255
    fmt.Println(ui8Min, ui8Max)
    // 0 255
}

3.1.2. 浮点类型

  • float32 单精度浮点类型
  • float64 双精度浮点类型

特点:

  1. 两者的表数范围和精度不一样。
  2. 都是有符号的类型,浮点数 = 符号位 + 指数位 + 尾数位
  3. 尾数部分可能丢失,导致精度损失
  4. 浮点字面量默认为 float64 类型
  5. 浮点类型的长度和表数范围是固定的,与平台无关。
var f1 = -0.00023234
var f2 float32 = 77882323.98
var f3 float64 = 77882323.98
fmt.Println(f1, f2, f3)
// -0.00023234 7.788232e+07 7.788232398e+07
// 1. 十进制表示法
f1 := 3.1415
f2 := 7.
f3 := .998
fmt.Println(f1, f2, f3)
// 3.1415 7 0.998

// 2. 科学计数表示法
f4 := 3.12e2
f5 := 3.12E2
f6 := 3.12e-2
fmt.Println(f4, f5, f6)
// 312 312 0.0312

3.1.3. 字符类型

无专门的字符型,使用 byte 来保存单个字母字符。
Go 的字符串是由字节组成的

  1. 要根据字符的码点值的大小来确定使用的整数类型
  2. 直接输出变量的结果是整型的码点值
  3. 需要使用格式化输出 %c
  4. 字符字面量是使用单引号包裹的单个字符,其类型是 int32
  5. Go 语言的编码都统一为了 UTF-8
var c1 byte = 'a'
fmt.Println(c1)
// 97
fmt.Printf("%c\n", c1)
// a

// var c2 byte = '中'
// 编译错误: constant 20013 overflows byte

var c2 int16 = '中'
fmt.Println(c2)
// 20013
fmt.Printf("%c\n", c2)
// 中


fmt.Println(reflect.TypeOf('a'), reflect.TypeOf('中'))
// int32 int32

3.1.4. 布尔类型

  1. bool 占一字节
var b bool = false
fmt.Println(b, unsafe.Sizeof(b))
// false 1

3.1.5. 指针类型

  • & 取地址
  • * 解引用
  • var p *int 定义指针变量
i := 10
fmt.Println("i 的地址", &i)
// i 的地址 0xc000018088

var p *int = &i
fmt.Println(p, *p)
// 0xc0000aa058 10

*p = 20
fmt.Println(p, *p, i)
// 0xc0000aa058 20 20
  • Go 的指针不能进行运算
  • Go 中只有值传递

3.2. 类型转换

3.2.1. 基本类型之间的转换

T(a)
// T 是类型, a 是变量或表达式
  1. Go 中没有自动类型提升,所有的类型转换都必须显示指定。
  2. 被转换的是变量或表达式的值,变量本身的类型没有发生变化。
  3. 表数范围大的值向表数范围小的类型转换时可能会发生溢出。
var i int32 = 1000
var f float32 = float32(i)
var i8 int8 = int8(i)
var i32 int32 = int32(i)

fmt.Printf("%v, %v, %v, %v\n", i, f, i8, i32)
// 1000, 1000, -24, 1000
  1. 因为没有自动类型转换,所以变量之间进行运算时必须保证类型一致。
var n1 int32 = 12
var n2 int64 = 100
// var n3 int32 = n1 + n2
// 编译错误: invalid operation: n1 + n2 (mismatched types int32 and int64)
var n3 int32 = n1 + int32(n2)

字面量和常量不用担心类型,会自动适应。但要注意结果可能会溢出。

var n1 int32 = 12
var n2 int8 = int8(n1) + 127
fmt.Println(n2)
// -117

const the127 = 127
var n1 int32 = 12
var n2 int8 = int8(n1) + the127
fmt.Println(n2)
// -117
fmt.Println(reflect.TypeOf(the127))
// int

如果字面量无法转换为目标类型,则编译报错

var n1 int32 = 12
var n2 int8 = int8(n1) + 128
// 编译错误: constant 128 overflows int8
fmt.Println(n2)

3.2.2. 其他基本类型转为 string

  1. fmt.Sprintf
a := 128
s := fmt.Sprintf("%d", a)
  1. strconv
func Itoa(x int) string

func FormatBool(b bool) string {
    if b {
        return "true"
    }
    return "false"
}

// i 待转换的整数; base 表数的进制, 可取 2-36
func FormatInt(i int64, base int) string
func FormatUint(i uint64, base int) string

// f 待转换的浮点数; fmt 表示方式; prec 精度, 小数点后几位 (四舍五入); bitSize f 原来占多少位 (32 for float32, 64 for float64)
func FormatFloat(f float64, fmt byte, prec, bitSize int) string

func FormatComplex(c complex128, fmt byte, prec, bitSize int) string
a := 128
s2 := strconv.FormatInt(int64(a), 2)
s10 := strconv.FormatInt(int64(a), 10)
fmt.Println(s2, s10)
// 10000000 128

f := 128.596
sf := strconv.FormatFloat(f, 'f', 2, 64)
fmt.Println(sf)
// 128.60

3.2.3. string 转为其他基本类型

  1. strconv
func Atoi(s string) (int, error)

func ParseBool(str string) (bool, error) {
    switch str {
    case "1", "t", "T", "true", "TRUE", "True":
        return true, nil
    case "0", "f", "F", "false", "FALSE", "False":
        return false, nil
    }
    return false, syntaxError("ParseBool", str)
}

// s 待转换的字符串; base 表数的进制; bitSize 转成多少位的整型 (0, 8, 16, 32, 64)
func ParseInt(s string, base int, bitSize int) (i int64, err error)
func ParseUint(s string, base int, bitSize int) (uint64, error)

// s 待转换的字符串; bitSize 转成多少位的浮点型 (32 for float32, or 64 for float64)
func ParseFloat(s string, bitSize int) (float64, error)

func ParseComplex(s string, bitSize int) (complex128, error) 
var i int32
i64, _ := strconv.ParseInt("125", 10, 32)
i = int32(i64)
fmt.Println(i)
// 125

var f float32
f64, _ := strconv.ParseFloat("1213.5567", 'f')
f = float32(f64)
fmt.Println(f)
// 1213.5566

3.2.4. string 与 []byte []rune 互转

bytes := []byte{48, 49, 50}
s := string(bytes)
fmt.Print(s) // 012

bytes = []byte(s)
fmt.Println(bytes) // [48 49 50]
runes := []rune{19990, 30028}
s := string(runes)
fmt.Println(s) // 世界

runes = []rune(s)
fmt.Println(runes) // [19990 30028]

3.3. 类型大小

  • bool 类型占 1 字节
  • intuint、指针类型的大小跟随系统字长
  • 空结构体的大小为 0,空结构体**独立作为局部变量(不被其他结构体所嵌套)**时的地址都是同一个,即 zerobase
    // runtime/malloc.go
    // base address for all 0-byte allocations
    var zerobase uintptr
    
type s struct {
}
type ss struct {
    s s
    i int
}

func main() {
    var i1 int
    var s1 s = s{}
    var i2 int
    var s2 s
    var i3 int

    fmt.Printf("%d %p\n", unsafe.Sizeof(s1), &s1) // 0 0xa1a560
    fmt.Printf("%d %p\n", unsafe.Sizeof(s2), &s2) // 0 0xa1a560
    fmt.Printf("%d %p\n", unsafe.Sizeof(i1), &i1) // 8 0xc0000a6058
    fmt.Printf("%d %p\n", unsafe.Sizeof(i2), &i2) // 8 0xc0000a6070
    fmt.Printf("%d %p\n", unsafe.Sizeof(i3), &i3) // 8 0xc0000a6078
    fmt.Println()

    var ss ss = ss{}
    fmt.Printf("%d %p\n", unsafe.Sizeof(ss), &ss)       // 8 0xc0000a60b0
    fmt.Printf("%d %p\n", unsafe.Sizeof(ss.s), &(ss.s)) // 0 0xc0000a60b0
    fmt.Printf("%d %p\n", unsafe.Sizeof(ss.i), &(ss.i)) // 8 0xc0000a60b0
}

4. 运算符

  • Go 中自增(++)和自减(--)只能单独作为一条语句使用,只有后序自增自减,没有前序的。
  • Go 中没有三元的条件运算符 ? :