第一部分在这里:十分钟速读 Effective Go (一)
7. 函数
多返回值特性
在其他语言中,通常会返回一个特殊值,比如 -1 这种,标识异常,但是 Golang 中直接支持返回多个值,其中一个值通常是表示是否存在错误
Golang
// 通常错误值是放在最后一个返回的
func (file *File) Write(b []byte) (n int, err error)
可命名返回值参数
Golang
// 返回值 n 和 err 已提前声明,函数中可直接使用,注意声明的值均为对应类型的默认值
func (file *File) Write(b []byte) (n int, err error) {
// return 不带返回值,则默认返回 n 和 err
return
}
Defer(延迟执行)机制
Golang
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err // 如果代码在这里 return f.Close() 不会被执行,因为他还没有通过 defer 注册
}
defer f.Close() // 声明一个延迟执行的代码块,他将在函数返回之前立即执行
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append is discussed later.
if err != nil {
if err == io.EOF {
break
}
return "", err // 如果在这里 return f.Close() 会被执行
}
}
return string(result), nil // 如果代码在这里 return f.Close() 也会被执行
}
8. 数据
利用 new / make 分配内存空间
new
为类型初始化了空间并返回其指针
Golang
// 等同于 p := &SyncedBuffer{},new 为类型初始化了空间并且初值都置为了0,然后返回其指针
p := new(SyncedBuffer)
// 这个与 new(SyncedBuffer) 不同,这个仅仅是声明一个变量,并没有为结构体初始化空间和值,而是一个 nil 指针
var p *SyncedBuffer
make
函数用于分配并初始化引用类型(如slice、map和channel)等,并且支持 初始化时指定长度
Golang
// arr 为*[]int64类型, arr 其实相当于 var arr *[]int64,在 Slice 底层并没有为 slice 的底层数组分配空间
arr := new([]int64)
*arr = append(*arr, 1)
ar := make([]int64, 0) // arr = &ar 在 Slice 底层为 slice 的底层数字分配了长度为 0 的空间
- 一种 Golang 独有的初始化结构体,slice,map 等的方式:
Composite literal
在中文中可以翻译为"复合字面量"或者"组合字面量"
go
// 其实就是最基本的声明并赋初值的一种简化语法操作,基础语法,入门必备,避免了使用 new,更简洁
slice := []int{1,2,3}
实际编码中,大量在使用
Composite literal
ormake
,极少用到new
数组
数组是 Slice 底层实现的基础,Golang 中数组不完全等同于 c 语言的数组,它有如下几个独特特点:
- Golang 中数组可以类比为一个值类型,将数组进行赋值时实际上执行的是值拷贝;
- 数组作为函数参数传递时,也会进行值拷贝传递,而非指针;
- 类型
[10]int
和[20]int
是不同的,数组的大小是其类型的一部分;
当然仍然可以通过
&array
方式传递数组指针,但是这并非 Go 官方推荐的做法,如果有此需求,直接使用 Slice 更好
Slice(切片)
- Slice 是对数组的二次封装,使其功能更强大,Slice 底层仍然是利用数组进行数据存储
- Slice 中存储的是数组的地址,所以 Slice 复制时,两个 Slice 指向了同样的底层数组,所以当函数传递的参数是 Slice 时,实际上传递的是数组的地址,子函数对 Slice 的修改可能会反馈到父函数中,为何这里是可能而不是一定呢,请看下面代码:
Golang
package main
import (
"fmt"
)
func main() {
slice := []int{1, 2}
slice = append(slice, 3) // slice 按照双倍扩容,所以下面的 cap 为 4
fmt.Println("cap", cap(slice))
passSlice(slice) // 第一次调用添加一个元素,未触发
fmt.Println("slice", slice)
slice = append(slice, 4)
fmt.Println("cap", cap(slice))
passSlice(slice)
fmt.Println("slice", slice)
}
func passSlice1(s []int) { // s 指向的底层数组与 slice 仍然一致,容量为 4,长度为 3
s = append(s, 4) // 未触发扩容,在原数组上增加了一个元素,容量为 4,长度为 4
s[0] = 100 // 修改数据,现在 s 和 slice 仍然共用一个底层数组,故该修改影响父函数的变量
fmt.Println("sub func slice:", s, "cap", cap(s))
}
func passSlice2(s []int) { // s 指向的底层数组与 slice 仍然一致,容量为 4,长度为 4
s = append(s, 4) // 触发扩容,容量扩为 8 个,长度为 5,注意此时底层数组地址已经变了
s[0] = 200 // 修改数据,现在 s 和 slice指向不同底层数组,故该修改不影响父函数的变量
fmt.Println("sub func slice:", s, "cap", cap(s))
}
// Output
cap 4
sub func slice: [100 2 3 4] cap 4
slice [100 2 3] // 可以看到外层数据被修改了
cap 4
sub func slice: [200 2 3 4 4] cap 8
slice [100 2 3 4] // 可以看到外层数据未被修改
上面的 append 函数实现是会根据 slice 的容量和长度动态扩容,扩容时会重新复制底层数组的数据
二维切片(数组)
二维切片其实就是数组的数组或者切片的切片,官方推荐了两种初始化方式,第一种针对会可能发生长度变化的二维切片
Golang
// Allocate the top-level slice.
picture := make([][]uint8, YSize) // 分配多少行
// Loop over the rows, allocating the slice for each row.
for i := range picture {
picture[i] = make([]uint8, XSize) // 分配每行的元素个数
}
第二种针对完全固定的长度的,可以一次性分配内存(非极致性能要求场景个人不推荐,这种写法可能对一些人理解起来费劲,性能影响也极少)
go
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// 创建一个完整的大 Slice
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// 这里需要注意,将大 Slice 切成小的 Slice 时,底层并不会重新分配内存,而是指向原来的 Slice 内存
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Map
map 基础用法这里不做赘述,因为 golang 的强类型机制,即使从 map 中获取不存在的值也会返回对应类型的默认值,这时候要利用 Golang 中特有的一种 comma ok
的写法来判断:
Golang
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok { // ok 为 true 才表示 map 中包含 key 为 tz 的值
return seconds
}
log.Println("unknown time zone:", tz)
return 0
}
delete(timeZone, "PDT") // Now on Standard Time
## []()
另外特别注意 map 是一种并发不安全的类型,
effective go
在该章节未做特别说明
打印输出
占位符 | 描述 |
---|---|
%d | 整数,可以是十进制、八进制或十六进制的数字 |
%f | 浮点数,可以指定小数点后的精度 |
%s | 字符串 |
%t | 布尔值 |
%v | (value)支持任意类型,输出等同于 fmt.Println |
%b | 二进制数 |
%x | 十六进制数 |
%o | 八进制数 |
%c | 字符 |
%T | 打印值类型 |
特别支持:
%+v
打印时会将结构体的字段名打印出来;%#v
会将包含结构体名称等打印出来
任何一个类型都可以实现func (m MyString) String() string
接口,然后自定义 %s/%v 等的输出内容
Golang
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}
a := MyString("aaa")
fmt.Printf("%v %+v %#v %T %#+q %s\n", a, a, a, a, a, a)
fmt.Println(a)
// Output, 可以看到自定义的 String 方法影响了大部分格式化输出
MyString=aaa MyString=aaa "aaa" `MyString=aaa` MyString=aaa
MyString=aaa
append 函数的补充说明
该函数原型如下,其中类型T是调用者决定的,这个函数无法通过目前的Go语法实现(不过泛型是可以实现类似能力的),所以它是个内建函数
scss
func append(slice []T, elements ...T) []T
注意该函数的返回值,与传入的值不一定是同一个,slice有可能在添加原始时动态扩容,返回一个新的地址; 另外该函数支持任意量元素添加,当我们期望将一个slice添加到另外一个slice后面时,需要利用...
展开操作符:
Golang
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
9. 初始化
Go中初始化其实就三个部分,分别为常量、变量和函数
常量
常量也分为全局和局部,但要求均是在编译器期间就可求值;常量只能是数字、字符(符文)、字符串或布尔值; 常量创建过程中有个特殊的 iota
值,是一个编译器预定义的标识符,当在一个包中第一次声明一个常量时,iota
会被设置为0。在后续的常量声明中,iota
会自动递增。它主要用于创建一系列相关的常量,例如枚举类型:
Golang
const (
Monday = iota // 0
Tuesday // 1
Wednesday // 2
Thursday // 3
Friday // 4
Saturday // 5
Sunday // 6
)
变量
变量其初始值也可以是在运行时才被计算,也可修改
初始化函数 init
- 可以定义多个init函数,会顺序执行
- init函数的执行是等该包中的所有变量声明都初始化完成后执行,所以init中很适合做参数检查和默认值
- 各个包中的init是按照该包导入顺序执行的,且只执行一次
10. 方法
- 可以为任何已命名的类型(除了指针或接口)定义方法; 接收者可不必为结构体;
Golang
type ByteSlice []byte
// 值方法
func (slice ByteSlice) Append(data []byte) []byte {
}
// 指针方法
func (slice *ByteSlice) Append(data []byte) (n int, err error) {
}
- 方法接受者可以为指针或值,区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用
Golang
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (r Person) method() {
fmt.Println("值方法")
}
func (r *Person) method2() {
fmt.Println("指针方法")
}
func main() {
var pv Person
pv.method2()
pv.method()
var p *Person
// p.method() // 这句会报错,
p.method2()
p = &pv
p.method() // 这里就不会报错了,因为 p 已经指向了具体的实例,
p.method2()
}
- 指针方法可以修改接收者;值方法调用它们会导致方法接收到该值的副本
11. 接口
Go 中的接口为指定对象的行为提供了一种方法;换句话说,如果一个结构体实现了某个接口定义的全部方法,那么它就是这个接口的一个具体实现;
Golang
type Sequence []int
// sort.Interface 所需的方法。任何类型只要实现了以下三个方法就支持使用 sort.Sort 进行排序
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// 用于打印输出的方法 - 在输出前对元素进行排序,再序列化。
func (s Sequence) String() string {
sort.Sort(s)
str := "["
for i, elem := range s {
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
类型转换
上面示例中 type Sequence []int
可以看出 Sequence
和 []int
其实是一样的,所以两者是可以互相转化的,所以上面的排序可以通过 sort.IntSlice
简化
Golang
type Sequence []int
// 用于打印输出的方法 - 在输出前对元素进行排序,再序列化。
func (s Sequence) String() string {
sort.IntSlice(s).Sort() // s 可以自动转化为 []int 类型
return fmt.Sprint([]int(s))
}
接口转换与类型断言
接口类型转换主要是两个场景,一是判断接口是什么类型,二是将接口转为具体的类型的值
- 判断接口类型,利用 switch
Golang
type Stringer interface {
String() string
}
var value interface{} // 调用者提供的值。
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
- 转换具体类型
Golang
str, ok := value.(string)
if ok {
fmt.Printf("字符串值为 %q\n", str)
} else {
fmt.Printf("该值非字符串\n")
}
接口的通用性
这个跟其他语言的接口目的其实是一致;接口可以使我们更加关注其行为而非实现,并且对于实现了同样接口的类型,可以做到快速替换;
接口和方法
由于几乎任何类型都能添加方法,因此几乎任何类型都能满足一个接口。一个很直观的例子就是 http 包中定义的 Handler 接口。任何实现了 Handler 的对象都能够处理 HTTP 请求。这里不再赘述。
12. 空白描述符
定义:空白标识符可被赋予或声明为任何类型的任何值,而其值会被无害地丢弃。它有点像 Unix 中的 /dev/null 文件:它表示只写的值,在需要变量但不需要实际值的地方用作占位符,在 Golang 中使用
-
来表示。
多重赋值中的空白标识符
比如函数返回多个有效值,其中某些值并不被使用,可以使用空白描述符,占位:
Golang
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
但是永远不要丢弃 err
Golang
// 如果路径不存在,那么这个程序会 panic
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s is a directory\n", path)
}
未使用的导入或者变量(通常用不到)
Golang
package main
import (
"fmt"
"io"
"log"
"os"
)
// 如果不加入下面的声明,fmt 包被导入就会被提示错误
var _ = fmt.Printf // For debugging; delete when done. // 用于调试,结束时删除。
var _ io.Reader // For debugging; delete when done. // 用于调试,结束时删除。
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}
为副作用(side effect)而导入
这个翻译很让人难以理解,但是目前都是用的这个词,具体就是导入某个库后,库里可能定义了 init 函数,会自动被执行,比如 pprof 库
Golang
import _ "net/http/pprof"
接口检查
Golang
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
13. 内嵌
Golang 中没有继承,子类父类直说,都是通过内嵌实现类似特性的,比如
接口内嵌
Golang
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter 接口结合了 Reader 和 Writer 接口。
type ReadWriter interface {
Reader
Writer
}
结构体内嵌
Golang
package main
import "fmt"
type Reader struct {
A string
}
func (r *Reader) Read() {
fmt.Println("==== read", r.A)
}
type Writer struct {}
func (r *Reader) Write() {
fmt.Println("==== write")
}
type WriterReader struct {
*Reader
*Writer
}
func main() {
wr := WriterReader{
// 特别注意,如果未对指针进行初始化,那么该值相当于nil,如果调用读取内部变量的方法会引起 panic
Reader: &Reader{
A: "a",
},
}
wr.Reader.Read() // 如果不初始化 Reader 变量,调用就会 panic
wr.Writer.Write() // 这个无论是否初始化,都不会 panic
}
当然上面的 WriterReader 结构体实现并不符合ReadWriter接口的定义,如果要满足其接口定义,则需要提升 Read和Write 方法到 WriterReader 结构体
golang
type ReadWriter struct {
reader *Reader
writer *Writer
}
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
注意,对于内嵌结构体,你也可以直接调用其方法:
Golang
wr.Read() // 完全可以直接调用
wr.Write()
// 另外一个示例
type Job struct {
Command string
*log.Logger
}
job.Log("starting now...")
job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...)) // 也可以使用内嵌名这样调用,直接忽略包限定名即可
命名冲突
如果内嵌的结构体中与外层变量或者方法存在冲突,则外层的会直接覆盖内嵌结构体内的,比如 Job 自己也定义了一个 Log 方法,那么 job.Log 引用的一定是自己定义的方法,当然如果 Job 自己又定义了一个 Logger 变量,那就会出现重复定义错误了,无法完成编译。
从国庆拖到了元旦,总算是又推进了一些,还剩几部分内容比较多的,争取元旦完成~