3

Effective Golang | shaoyuanhangyes

 1 year ago
source link: https://shaoyuanhangyes.github.io/2022/04/11/Effective-Golang/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Effective Golang

2022-04-112022-05-09Go

Go语言精进之路

以构建二进制可执行文件为目的

GoProject
|--LICENSE
|--Makefile
|--README.md
|--cmd/
|--app1/
|--main.go
|--app2/
|--main.go
|--go.mod
|--go.sum
|--pkg/
|--lib1/
|--lib1.go
|--lib2/
|--lib2.go

cmd目录存放项目要构建的可执行文件对应的main包的源文件
cmd目录下的各app的main包将整个项目的依赖连接在一起
并且通常来说main包应该很简洁
我们会在main包中做一些命令行参数解析 资源初始化 日志设施初始化 数据库连接初始化等工作
之后就会将程序的执行权限交给更高级的执行控制对象
有一些Go项目将cmd这个名字改为app 但其功用并没有变

Makefile是项目构建工具的脚本 Go没有内置的例如CMake等级别的项目构建工具
在Go典型项目中 项目构建工具的脚本一般放在项目顶层目录下

go.mod go.sum Go 包依赖管理 使用的配置文件

pkg目录 存放项目自身要使用并且同样也是可执行文件对应main包要依赖的库文件
该目录下的包可以被外部项目引用 算是项目导出包的一个聚合

以只构建库为目的

GoLibProject
|--LICENSE
|--Makefile
|--README.md
|--go.mod
|--go.sum
|--lib.go
|--lib1/
|--lib1.go
|--lib2/
|--lib2.go
|--internal
|--ilib1/
|--ilib2/

为何去除cmd和pkg两个子目录
因为只构建库所以没必要存放二进制文件main包源文件的cmd目录
因为Go库项目的初衷一般是对外部暴露API 因此没必要将其单独聚合在pkg目录下

若一些包不想暴露给外部引用 仅限项目内部使用 可以引入internal包机制实现
在顶层加入一个internal目录 将不想暴露到外部的包都放在该目录下

使用gofmt即可帮助规范化代码 但gofmt工具无法自动删减文件头部的包导入目标
但官方拥有goimports 可根据源码的最新变动自动从导入包列表中增删包

包package以小写形式的某个单词命名
包名可以不唯一 但尽量与包导入路径的最后分段保持一致

小驼峰拼写法 lowCamelCase

变量名字中不要带有类型信息

Go语言中的接口是Go在编程语言层面的一个创新,它为Go代码提供了强大的解耦合能力,因此良好的接口类型设计和接口组合是Go程序设计的静态骨架和基础。良好的接口设计自然离不开良好的接口命名。在Go语言中,对于接口类型优先以单个单词命名。对于拥有唯一方法(method)或通过多个拥有唯一方法的接口组合而成的接口,Go语言的惯例是用“方法名+er”命名。比如:

type Writer interface {
Write(p []byte) (n int, err error)
}
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}

使用一致的变量声明形式

Go语言有两类变量
包级变量 在package级别可见的变量 若是导出变量则该包级变量也可以被视为全局变量
局部变量 函数或方法体内声明的变量 仅在函数和方法体内可见

包级变量声明形式

声明的同时显示初始化 若不初始化 则会被保证拥有同类型的”零值”

//第一种
var a int32 = 17
var f float32 = 3.14

//第二种
var a = int32(17)
var f = float32(3.14)

官方更推荐后者 尤其是将这些变量放在同一个var块中声明时

并且将同一类型的声明放在同一个var块中

var (
bufioReaderPool sync.Pool
bufioWriter2kPoll sync.Pool
bufioWriter4kPool sync.Pool
)

var (
aLongTimeAgo = time.Unix(1,0)
noDeadline = time.Time{}
noCancel = (chan struct{})(nil)
)
声明决策流程

无类型常量

定义常量使用const关键词

const (
num1 int = 0
str1 string = "abc"
)

绝大多数情况下 Go常量在声明时并不显式指定类型
即无类型常量

const (
SeekStart = 0
SeekCurrent = 1
SeekEnd = 2
)

有类型常量的烦恼

为什么要使用无类型常量呢 因为在Go语言中 两个类型即便拥有相同的底层类型 也仍然是不同的数据类型 因此不能在一个表达式中进行运算

type myInt int 
func main() {
var a int = 5
var b myInt = 8
fmt.Println(a + b) //编译器报错
}

Go在处理不同类型的变量间运算时 不支持隐式的类型转换
若要解决上面的编译错误 则必须进行显示类型转换

type myInt int 
func main() {
var a int = 5
var b myInt = 8
fmt.Println(a + int(b)) //输出13
}

同理 将有类型常量和变量一起运算时 也要遵循此规则
若两个类型不同 也会报错

type myInt int
const n myInt = 13
const m int = n + 5 //编译器报错
func main() {
var a int = 5
fmt.Println(a + n) //编译器报错
}

必须显式类型转换后才能通过编译

type myInt int
const n myInt = 13
const m int = int(n) + 5
func main() {
var a int = 5
fmt.Println(a + int(n))
}

无类型常量简化代码

type myInt int
type myFloat float32
type myString string

func main() {
var j myInt = 5
var f myFloat = 3.14
var str myString = "syh"
}

由此可见 5 3.14 syh 无需类型转换就可以直接赋值给j f str 等价于下面的代码

var j myInt = myInt(5)
var f myFloat = myFloat(3.14)
var str myString = myString("syh")

因此无类型常量使混合数据类型运算变的更加灵活 代码也有所简化

无类型常量同样也拥有自己的默认类型

无类型的布偶常量 bool
无类型的整数常量 int
无类型的字符常量 int32(rune)
无类型的浮点数常量 float64
无类型的复数常量 complex128
无类型的字符串常量 string

若常量被赋值给无类型变量 or 接口变量
常量的默认类型对于确定无类型变量的类型及接口对应的动态类型是至关重要的

const (
a = 5
b = "Ruojhen"
)
func main() {
n := a
var i interface{} = a

fmt.Printf("%T\n", n) //输出int
fmt.Printf("%T\n", i) //输出int
i = s
fmt.Printf("%T\n", i) //输出string
}

iota实现枚举常量

C/C++中枚举常量的定义类型如下

enum Weekday {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
};
//c++没有显式的给枚举常量赋初始值 所以第一个常量值为0 后续常量依次+1
ing main() {
enum Weekday d = Saturday;
cout<<d; // 输出6
}

而Go中没有提供定义枚举常量的语法 通常使用常量来定义枚举常量

const (
Sunday = 0
Monday = 1
Tuesday = 2
Wednesday = 3
Thursday = 4
Friday = 5
Saturday = 6
)

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

const (
Apple, Banana = 11, 22
Strawberry, Grape
Pear, Watermelon
)

尽管常量定义的后两行没有显式的赋初值 但Go编译器将其隐式使用第一行的表达式 因此上述代码等价为

const (
Apple, Banana = 11, 22
Strawberry, Grape = 11, 22
Pear, Watermelon = 11, 22
)

iota是Go的一个预定义标识符 表示const声明块中每个常量所处位置在块中的偏移量
同时每一行中的iota自身也是一个无类型常量 可以自动参与不同类型的求值运算 而无须进行显式类型转换

下面是Go标准库中sync/mutex.go中的一段枚举常量的定义

// $GOROOT/src/sync/mutex.go
const (
mutexLocked = 1 <<iota
mutexWoken
mutexStarving
mutexWaiterShift = iota
starvationThresholdNs = 1e6
)

第一行 mutexLocked = 1 <<iota 因为iota表示的是所处位置在const块中的偏移量 因此第一行的iota=0 所以mutexLocked = 1 <<iota = 1<<0 = 1

第二行 mutexWoken没有显式的赋予初值 所以会隐式重复前一个非空表达式 即等价于 mutexWoken = 1<<iota 而在第二行中的iota=1 所以mutexWoken = 1<<iota = 1<<1 = 2

第三行 同理 mutexStarving没有显式的赋予初值 所以等价于mutexStarving = 1<<iota 而在第三行中的iota=2 所以mutexStarving = 1<<iota = 1<<2 = 4

第四行 mutexWaiterShift = iota 第四行中的iota=3 因此mutexWaiterShift = iota = 3

位于同一行的iota即便出现多次 其值也是一样的

const (
Apple, Banana = iota, iota + 10 // 0, 10 (iota = 0)
Strawberry, Grape // 1, 11 (iota = 1)
Pear, Watermelon // 2, 12 (iota = 2)
)

如果要略过iota = 0 而从iota = 1开始正式定义枚举常量 可以效仿下面的代码

// $GOROOT/src/syscall/net_js.go,go 1.12.7

const (
_ = iota
IPV6_V6ONLY // 1
SOMAXCONN // 2
SO_ERROR // 3
)

如果要定义非连续枚举值 也可以使用类似方式略过某一枚举值

const (
_ = iota // 0
Pin1
Pin2
Pin3
_ // 相当于_ = iota,略过了4这个枚举值
Pin5 // 5
)

iota使得Go在枚举常量定义上的表达力大增

iota预定义标识符能够以更为灵活的形式为枚举常量赋初值 在C++中就不那么灵活

enum Season {
spring,
summer = spring + 2,
fall = spring + 3,
winter = fall + 1
};
// 若要对winter求值 就必须向上查询fall summer spring的值

Go的枚举常量不限于整型值 还可以定义浮点型的枚举常量 而C++就无法定义浮点类型的enum Go之所以可以定义浮点型是因为Go的无类型常量

const (
PI = 3.1415926 // π
PI_2 = 3.1415926 / (2 * iota) // π/2
PI_4 // π/4
)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK