/

Go 语言第一课

02 设计哲学

设计哲学之于编程语言,就好比一个人的价值观之于这个人的行为。

  • 简单:Go 生产力的源泉。
  • 显式:Go 希望开发人员 明确知道自己在做什么;显式的基于值比较的错误处理方案。
  • 组合:类型嵌入(Type Embedding)。
  • 并发:面向多核、原生支持并发、用户层轻量级线程 goroutine。
  • 面向工程:将解决工程问题作为 Go 的 设计原则之一,这些问题包括:程序构建慢、依赖管理失控、代码难于理 解、跨语言构建难等。

03 配好环境

安装多个 Go 版本

go get golang.org/dl/go1.15.13
go1.15.13 download
go1.15.13 version

配置 Go

go env
go help environment

04 Go 程序的结构

  • import "fmt" 一行中 fmt 代表的是包的导入路径(Import),它表示的是标准库下的 fmt 目录,整个 import 声明语句的含义是导入标准库 fmt 目录下的包
  • fmt.Println 函数调用一行中的 fmt 代表的则是包名。
  • 通常导入路径的最后一个分段名与包名是相同的,这也很容易让人误解 import 声明语句中的 fmt 指的是包名,其实并不是这样的。
gofmt main.go

Go module

go mod init
go mod tidy

05 Go 项目的布局标准

loccount 工具

https://github.com/golang/go

tree -LF 1 .

06 解决包依赖管理

GOPATH -> Vendor -> Go Module

GOPATH

go env

GOPATH="/Users/v_yfanzhao/go"

go get github.com/sirupsen/logrus

vendor

  • Go 项目必须放在 GOPATH 环境变量配置的路径下,庞大的 vendor 目录需要提交到代码仓库,不仅占用代码仓库空间,减慢仓库下载和更新的速度, 而且还会干扰代码评审,对实施代码统计等开发者效能工具也有比较大影响。
  • 你还需要手工管理 vendor 下面的 Go 依赖包,包括项目依赖包的分析、版本的记 录、依赖包获取和存放,等等,最让开发者头疼的就是这一点。

Go Module

Go Module 与 go.mod 是一一对应的。go.mod 文件所在的顶层目录也被称为 module 的根目录,module 根目录以及它子目录 下的所有 Go 包均归属于这个 Go Module,这个 module 也被称为 main module。

package main

import "github.com/sirupsen/logrus"

func main() {
logrus.Println("hello, go module mode")
}
go mod init
go mod tidy
module go-lesson-one

go 1.17

require github.com/sirupsen/logrus v1.9.0

require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect

major.minor.patch

Go 的语义导入版本机制:将包主版本号引入到包导入路径中。v0、v1 时不加入路径。

因此甚至可以同时依赖一个包的两个不兼容版本:

import (
"github.com/sirupsen/logrus"
logv2 "github.com/sirupsen/logrus/v2"
)

Go 会在该项目依赖项的所有版本中,选出符合项目整体要求的“最小版本”。这与 PHP Composer 最新最大 (Latest Greatest) 版本 相反。

07 Go Module 操作

go list -m all

go list -m -versions github.com/sirupsen/logrus

# 指定版本 升降级
go get github.com/sirupsen/logrus@v1.7.0

# 指定版本 升降级
go mod edit -require=github.com/sirupsen/logrus@v1.7.0
go mod tidy

使用 vendor 机制

go mod vendor

go build -mod=verdor
# 顶层目录下存在 vendor 目录,那么 go build 默认也会优先基于 vendor 构建,除非:
go build -mod=mod

08 Go 程序的执行次序

可执行程序的 main 包必须定义 main 函数,否则 Go 编译器会报错。

除了 main 包外,其他包也可以拥有自己的名为 main 的函数 或方法。

init 函数

除了前面讲过的 main.main 函数之外,Go 语言还有一个特殊函数,它就是用于进行包初始化的 init 函数了。main 函数之前,常量和变量初 始化之后。每个 init 函数在整个 Go 程序生命周期内仅会被执行一次。Go 包可以拥有不止一个 init 函数。

Go 在进行包初始化的过程中,会采用“深度优先”的原则,递归初始化各个包的 依赖包。

package main
|- import pkg1
|- import pkg2
|- const
|- var
|- init()
|- const
|- var
|- init()
|- const
|- var
|- init()
|- main()

init 函数的用途

  • 重置包级变量值。被用于检查包级变量的初始状态。
  • 实现对包级变量的复杂初始化。
  • 在 init 函数中实现“注册模式”。通过在 init 函数中注册自己的实现的模式,就有效降低了 Go 包对外的直接 暴露,尤其是包级变量的暴露,从而避免了外部通过包级变量对包状态的改动。

09 构建一个 Web 服务

package main

import (
"net/http"
)

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
})
http.ListenAndServe(":8888", nil)
}
curl localhost:8888
Hello World

https://github.com/imzyf/go-bookstore

10 变量声明

var a int = 10
// 将变量名放在了类型的前面
// 修饰关键字 变量名 类型 初值

// 省略类型信息的声明
var b = 12
// 显式赋予变量初值
var b = int32(13)
// 声明多个
var a, b, c = 12, 'A', "hello"

// 短变量声明
a := 12
b := 'A'
c := "hello"
// 声明多个
a, b, c := 12, 'A', "hello"

Go 语言的两类变量

  • 包级变量 (package varible)
  • 局部变量 (local varible)

包级变量的声明形式

包级变量只能使用带有 var 关键字的变量声明形式,不能使用短变量声明形式,但在形式细节上可以有一定灵活度。

var b int32 = 17 // 显式指定类型
var f float32 = 3.14 // 显式指定类型

var a = 13 // 使用默认类型
var b = int32(17) // 显式指定类型
var f = float32(3.14) // 显式指定类型

var a int32
var f float64
// 声明聚类
var (
netGo bool
netCgo bool
)

var (
aLongTimeAgo = time.Unix(1, 0)
noDeadline = time.Time{}
noCancel = (chan struct{})(nil)
)

// 就近原则
// 尽可能在靠近第一次使用变量的位置声明这个变量

局部变量的声明形式

// 延迟初始化的局部变量
var err error

// 显式初始化的局部变量
a := 17
f := float32(3.14)
s := []byte("hello, gopher!")
// 尽量在分支控制时使用短变量声明形式

11 代码块 Block 与作用域 Scope

// 变量遮蔽
var a = 11
func foo(n int) {
a := 1
a += n
}
func main() {
fmt.Println("a =", a) // 11
foo(5)
fmt.Println("after calling foo, a =", a) // 11
}
  • 宇宙代码块(Universe Block)
  • 包代码块(Package Block)
  • 文件代码块(File Block)
  • 分支控制语句隐式代码块
  • switch/select 的子句隐式代码块

一个标识符的作用域就是指:这个标识符在被声明后可以被有效使用的源码区域。

导出标识符:

  • 声明在包代码块中
  • 它名字第一个字符是一个大写的 Unicode 字符

https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter11/main.go

12 数值类型

整型

Go 采用补码(2’s complement)作为整型的比特位编码方法。Go 的补码是通过原码逐位取反后再加 1 得到的。

unit8  1 0 0 0 0 0 0 1 = 129
int8 1 0 0 0 0 0 0 1 = -127

0 1 1 1 1 1 1 1 127
1 0 0 0 0 0 0 0 取反
1 0 0 0 0 0 0 1 +1 -127

整型的溢出问题

https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter12/main.go

这个问题最容易发生在循环语句的结束条件判断中,因为这也是经常使用整型变量的地方。

浮点型

IEEE 754

符号位Sign 阶码Exponent 尾数Maintissa
\bit 位\符号位阶码阶码偏移值尾数
单精度 float321812723
双精度 float64111102352

eg:129.8125

步骤一:我们要把这个浮点数值的整数部分和小数部分,分别转换为二进制形式(后缀 d 表示十进制数,后缀 b 表示二进制数):

整数部分:139d => 10001011b;

小数部分:0.8125d => 0.1101b(十进制小数转换为二进制可采用“乘 2 取整”的竖式计算)。
0.8125 * 2 = 1.625 …… 1
0.625 * 2 = 1.25 …… 1
0.25 * 2 = 0.5 …… 0
0.5 * 2 = 1 …… 1

139.8125d -> 10001011.1101b
步骤二:移动小数点,直到整数部分仅有一个 1。

10001011.1101b -> 1.00010111101b

小数点向左移了 7 位,这样 指数就为 `7`,尾数为 `00010111101b`。
步骤三:计算阶码。对于 float32 的单精度浮点数而言:
阶码 = 指数 + 偏移值
偏移值的计算公式为 2^(e-1)-1,其中 e 为阶码部分的 bit 位数,这里为 8,于是单精度浮点数的阶码偏移 值就为 2^(8-1)-1 = 127。

阶码 = `7` + 127 = 134d = `10000110b`。
步骤四:将符号位、阶码和尾数填到各自位置,得到最终浮点数的二进制表示

符号位 0
阶码 10000110
尾数 00010111101 不足 23 位补零 `0_0010111101_00_0000000000`

139.8125 -> 0_10000110_00010111101_000000000000

复数型

矢量计算。

创建自定义的数值类型

type MyInt int32

var m int = 5
var n int32 = 6
var a MyInt = m // error
var a MyInt = n // error

var a = MyInt(m) // ok
var a = MyInt(n) // ok

MyInt 类型的底层类型是 int32,所以它的数值性质与 int32 完全相同,但它 们仍然是完全不同的两种类型。

类型别名(Type Alias)

type MyInt = int32

var n int32 = 6
var a MyInt = n

通过类型别名语法定义的新类型与原类型别无二致,可以完全相互替代。

13 字符串类型

why-what-how

非原生字符串:

  • 不是原生类型,编译器不会对它进行类型校验,导致类型安全性差;
  • 字符串操作时要时刻考虑结尾的 \0,防止缓冲区溢出;
  • 以字符数组形式定义的“字符串”,它的值是可变的,在并发场景中需要考虑同步问题;
  • 获取一个字符串的长度代价较大,通常是 O(n) 时间复杂度;
  • C 语言没有内置对非 ASCII 字符(如中文字符)的支持。

string 类型的数据是不可变的,提高了字符串的并发安全性和存储利用率(同一个字符串值分配同一块存储)。

var s string = "hello"
s[0] = 'k' // cannot assign to s[0] (value of type byte)
s = "gopher" // ok

没有结尾 \0,而且获取长度的时间复杂度是常数时间,消除了获取字符串长度的开销。

反引号原生支持“所见即所得”的原始字符串,大大降低构造多行字符串时的心智负担。

对非 ASCII 字符提供原生支持,消除了源码在不同环境下显示乱码的可能。Unicode 字符是以 UTF-8 编码格式存储在内存。

通过单引号括起的字符字面值:

https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter13/main.go

UTF-8 编码解决的是 Unicode 码点值在计算机中如何存储和表示(位模式)的问题。UTF-8 方案使用变长度字节,从 1 个到 4 个不等。

一个 rune 存储一个 unicode 码点或 utf-32 的四字节编码;从字节视角,string 对应的底层存储存放的是 utf8 编码。

Go 字符串类型的内部标示

// StringHeader 是一个 string 的运行时表示
// string 类型其实是一个“描述符”
type StringHeader struct {
// 一个指向底层存储的指针
Data uintptr
// 字符串的长度
Len int
}

直接将 string 类型通过函数或方法参数传入也不会带来太多的开销。因为传入的仅仅是一个“描述符”,而不是真正的字符串数据。

Go 字符串类型的常见操作

下标操作;下标操作,我们获取的是字符串中特定下标上的字节,而不是字符。

字符迭代:

  • or 迭代,字节视角的迭代
  • 字符串中 Unicode 字符的码点值,以及该字符在字符串中的偏移值(字节视角)

字符串连接;+ += strings.Builder strings.Join fmt.Sprintf

字符串比较;= =、!= 、>=、<=、> 和 <。

字符串转换;string -> []rune []byte

14 常量

  • 支持无类型常量
  • 支持隐式自动转型
  • 可用于实现枚举
type myInt int
// 无类型常量(Untyped Constant)
const n = 13

func main() {
var a myInt = 5
// 隐式转型
fmt.Println(a + n)
}

// 无类型常量 + 隐式转型:使得在 Go 这样的具有强类型系统的语言,在处理表达式混合数据类型运算的时候具有比较大的灵活性,代码编写也得到了一定程度的简化。

Go 的 const 语法提供了“隐式重复前一个非空表达式”的机制。

const (
Apple, Banana = iota, iota + 10 // 0, 10 (iota = 0)
Strawberry, Grape // 1, 11 (iota = 1)
Pear, Watermelon // 2, 12 (iota = 2)
)
const (
_ = iota // 略过 iota = 0
IPV6_V6ONLY // 1
SOMAXCONN // 2
SO_ERROR // 3
)

15 数组与切片

数组是一个固定长度的、由同构类型元素组成的连续序列。不仅是逻辑上的连续序列,而且在实际内存分配时也占据着一整块内存。

切片不定长同构数据类型。切片可以看成是数组的“描述符”(句柄),为数组打开了一个访问与修改的“窗口”。

// 切片
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 切片的长度,即切片中当前元素的个数
cap int // 底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值
}
var sl1 []int // 是声明,未初始化,是nil值,底层没有分配内存空间
var sl2 = []int{} // 初始化了,不是nil值,底层分配了内存空间,有地址。

16 map 类型

一组无序的键值对。

map[key_type]value_type

key 的类型必须支持“==”和“!=”两种比较操作符。

References

– EOF —