0%

Go常用

不定时维护 Go 常用

名词解释

  • 指针方法集:我们指定这种声明 func (u *User) Notify() error ,为指针方法集*User
  • 值方法集:我们指定这种声明 func (u User) Notify() error ,为值方法集User
  • 结构类型:我们指定多个类型的组合为结构类型,或外部类型,类似 Admin 这种声明
  • 接收器:我们称方法集对应的接收者为接收器,*User指针方法集由所有具有*User接收器和User接收器类型的方法组成

& 和 *

  • & 代表取变量的内存地址
  • * 代表根据地址取得地址指向的值,* 是指针操作符,代表一个变量是指针类型,* 只能加在地址前方根据地址取出地址指向的值,_和&互相抵消,即 _&a = a
  • golang 中只有值传递
package main

import(
"fmt"
)


func modify(a int) {
a = 11
fmt.Println(a) // 11
}

func modify2(a *int) { // a *int 代表a是指针类型,只接受传入类似 &a 的地址
*a = 12
fmt.Println(a) // 0xc000014098
}

func main() {
a := 1
modify(a)
fmt.Println(a) // 1
modify2(&a)
fmt.Println(a) // 12
*&a = 13
fmt.Println(a) // 13
}

全局变量

var a int

func main() {
a = 4
fmt.Println(a)
a := 5
fmt.Println(a)
}

结果

4
5

interface

当接口仅包含一个方法时,这是 Go 中的约定,以-er 后缀命名接口

type Notifier interface {
Notify() error
}

该函数SendNotification接受实现了Notifier接口的类型的任何值或指针(具体使用值还是指针见:实现接口规则

func SendNotification(notify Notifier) error {
return notify.Notify()
}

如上代码,规定SendNotification函数的入参对象必须实现Notifier接口的所有方法(很明显不实现怎么会有.Notify()方法呢),此函数可以为实现了接口的结构体(如 User)值或指针(如 bill)执行接口的特定方法
假如使用User结构体调用SendNotification方法时,如下 SendNotification(bill)

func main() {
bill := User{"Bill", "[email protected]"}
SendNotification(bill)
fmt.Print(bill)
}

User结构体必须实现Notifier接口的所有方法,如下
实现方式 1:(接收器为指针类型)

func (u *User) Notify() error {
fmt.Printf("User: Sending User Email To %s<%s>\n", u.Name, u.Email)
u.Name = "BillEdit"
u.Email = "[email protected]"
return nil
}

实现方式 2:(接收器为值类型)

func (u User) Notify() error {
fmt.Printf("User: Sending User Email To %s<%s>\n", u.Name, u.Email)
u.Name = "BillEdit"
u.Email = "[email protected]"
return nil
}

实现接口规则

函数所接受的实现了接口的类型,取决于选用的方法集是 *User 指针类型(&User{"Bill", "[email protected]"})还是 User 值类型(User{"Bill", "[email protected]"})

  • User 值类型的方法集,不包含接收器类型为*User 类型的方法,见下方代码验证
    • 调用SendMsg行会报错:cannot use (User literal) (value of type User) as Notifier value in argument to SendMsg: missing method Msg (Msg has pointer receiver)
    • 因为 User 值类型的方法集只包含 Notify() 方法,并不包含 Msg() 方法,所以传入SendMsg 中的 User 值类型并没有实现Notifier 接口的全部方法,无法调用接口中的方法
type Notifier interface {
Notify() error
Msg() error
}

type User struct {
Name string
Email string
}

func (u User) Notify() error {
fmt.Printf("Notify: Sending User Email To %s<%s>\n", u.Name, u.Email)
return nil
}

func (u *User) Msg() error {
fmt.Printf("Msg: Sending User Email To %s<%s>\n", u.Name, u.Email)
return nil
}

func main() {
SendMsg(User{"Bill", "[email protected]"})
}

func SendNotification(notify Notifier) error {
notify.Notify()
return nil
}

func SendMsg(notify Notifier) error {
notify.Msg()
return nil
}
  • *User 指针类型的方法集,包含接收器类型为User*User 的方法,见下方代码验证
    • 调用SendNotification行并不会报错
    • 因为 *User 指针类型的方法集包含 Notify() 方法和 Msg() 方法,所以,传入SendNotification 中的 *User 指针类型实现了 Notifier 接口的全部方法
type Notifier interface {
Notify() error
Msg() error
}

type User struct {
Name string
Email string
}

func (u User) Notify() error {
fmt.Printf("Notify: Sending User Email To %s<%s>\n", u.Name, u.Email)
return nil
}

func (u *User) Msg() error {
fmt.Printf("Msg: Sending User Email To %s<%s>\n", u.Name, u.Email)
return nil
}

func main() {
SendNotification(&User{"Bill", "[email protected]"})
}

func SendNotification(notify Notifier) error {
notify.Notify()
return nil
}

func SendMsg(notif Notifier) error {
notif.Msg()
return nil
}

结果

Notify: Sending User Email To Bill<[email protected]>

接口的魔力

  • 可以使用接口来减少行为(调用接口方法)与该行为的实现(实现接口方法)之间的耦合

示例,普通调用方式:

book := Book{"Alice in Wonderland", "Lewis Carrol"}
log.Println(book.String())

接口调用方式:

book := Book{"Alice in Wonderland", "Lewis Carrol"}
var s fmt.Stringer = book
log.Println(s.String())

接口调用方式时,接口成为方法实现和方法调用之间的桥梁,实现调用方法(通过调用接口中的方法实现调用方法)松耦合于实现方法

  • 可以通过两个类型实现同一个接口,达到调用一个接口方法,分别调用两个类型的方法的效果

示例,通过调用接口的 String 方法,实现传递不同的实现执行不同的功能

package main

import (
"fmt"
"strconv"
"log"
)

// 定义Book类型
type Book struct {
Title string
Author string
}
// 实现 fmt.Stringer 接口
func (b Book) String() string {
return fmt.Sprintf("Book: %s - %s", b.Title, b.Author)
}

// Declare a Count type which satisfies the fmt.Stringer interface.
type Count int

func (c Count) String() string {
return strconv.Itoa(int(c))
}

// Declare a WriteLog() function which takes any object that satisfies
// the fmt.Stringer interface as a parameter.
func WriteLog(s fmt.Stringer) {
log.Println(s.String())
}

func main() {
// Initialize a Count object and pass it to WriteLog().
book := Book{"Alice in Wonderland", "Lewis Carrol"}
WriteLog(book)

// Initialize a Count object and pass it to WriteLog().
count := Count(3)
WriteLog(count)
}

结果

Book: Alice in Wonderland - Lewis Carrol
3

入参

Notify 的入参和传参类型必须全为指针或全不为指针

type User struct {
Name string
Email string
}

func Notify(u *User) error {
fmt.Printf("User: Sending User Email To %s<%s>\n", u.Name, u.Email)
u.Name = "BillEdit"
u.Email = "[email protected]"
return nil
}

func main() {
bill := &User{"Bill", "[email protected]"}
Notify(bill)
fmt.Print(bill)
}

结果

User: Sending User Email To Bill<[email protected]>
&{BillEdit [email protected]}

接收器

接收器类型(如(u *User))和调用者不允许指针和值混用

  • 不允许func (u User) Notify() error(&User{"Bill", "[email protected]"}).Notify()
  • 不允许func (u *User) Notify() error(User{"Bill", "[email protected]"}).Notify()
  • 但允许定义中间变量(如bill := User{"Bill", "[email protected]"})调用func (u *User) Notify() error,猜测 go 进行了自动转换bill为指针类型
  • Notify()方法内部对u的操作是否生效取决于定义的接收器是否为指针类型
type User struct {
Name string
Email string
}

func (u *User) Notify() error {
fmt.Printf("User: Sending User Email To %s<%s>\n", u.Name, u.Email)
u.Name = "BillEdit"
u.Email = "[email protected]"
return nil
}

func main() {
bill := User{"Bill", "[email protected]"}
bill.Notify()
fmt.Print(bill)
}

结果

User: Sending User Email To Bill<[email protected]>
{BillEdit [email protected]}

原文链接:https://www.ardanlabs.com/blog/2014/05/methods-interfaces-and-embedded-types.html

嵌入类型

声明一个新类型,将 User 类型嵌入其中,我们称 Admin 为结构类型,称 User 为类型

type Admin struct {
User
Level string
}
  • 嵌入类型不是继承,而是组合,AdminUser 类型之间没有关系
  • 当我们嵌入一个类型时,如 User 嵌入 Admin ,则 User 类型的方法成为外部类型 Admin 的方法,但是当方法被调用时,方法的接收者仍然是内部类型 User,而不是外部类型 Admin ,如下仍可调用 SendNotificationAdmin 类型通过提升嵌入的 User 类型的方法来实现接口
func main() {
admin := &Admin{
User: User{
Name: "john smith",
Email: "[email protected]",
},
Level: "super",
}

SendNotification(admin)
}
admin.Notify()
  • 嵌入类型的名称 User 充当字段名称,并且嵌入类型作为内部类型存在,因此下方调用方式也正确
admin.User.Notify()

方法集提升的规则

给定一个结构类型 AdminUser 类型,如果 Admin 结构类型包含匿名字段 User

  • Admin 的方法集只包含接收者为 User 的提升方法
  • *Admin 的方法集包含接收器为 *User 和接收器为 User 的提升方法

内部类型和外部类型均满足接口的实现

如果外部类型包含满足接口的实现,则将使用它。否则,由于方法提升,任何实现接口的内部类型都可以通过外部类型使用

函数

func [(obj Class)] FnName (p1 ParamType [, p2 ParamType]) ReturnType [, ReturnType] {
}
  • 具名函数匿名函数
// 具名函数
func Add(a, b int) int {
return a+b
}

// 匿名函数
var Add = func(a, b int) int {
return a+b
}
  • 多个参数和多个返回值
// 多个参数和多个返回值
func Swap(a, b int) (int, int) {
return b, a
}
  • File 类型接收器定义的 File 类型独有的方法,而非 File 对象方法
// 读文件数据
func (f *File) Read(offset int64, data []byte) int {
// ...
}

每种类型对应的方法必须和类型的定义在同一个包中,因此是无法给 int 这类内置类型添加方法的(因为方法的定义和类型的定义不在一个包中)

调用栈

  • 在 x86_64 的机器上使用 C 语言中调用函数时,参数都是通过寄存器和栈传递的,其中:
    • 六个以及六个以下的参数会按照顺序分别使用 edi、esi、edx、ecx、r8d 和 r9d 六个寄存器传递;
    • 六个以上的参数会使用传递,函数的参数会以从右到左的顺序依次存入栈中;
    • 函数的返回值是通过 eax 寄存器进行传递的,只能保存一个值
  • Go 语言只使用栈传递参数和接收返回值,可通过增加栈的空间大小增加返回值个数
    • 通过栈传递参数,入栈的顺序是从右到左,而参数的计算是从左到右
    • 函数返回值通过堆栈传递并由调用者预先分配内存空间
    • Go 语言中的函数传参都是值传递,整型、数组类型、结构体和指针都是值传递的,调用函数时接收方收到参数会对内容进行复制。
    • 传递结构体时:会拷贝结构体中的全部内容,在传递内存占用非常大的结构体时,应尽量使用指针作为参数,来避免数据复制影响性能
    • 传递结构体指针时:会拷贝结构体指针,相当于同时出现两个指针指向原有的内存空间,修改指针对应的值时相当于通过指针找到结构体后再修改
  • 优缺点:
    • C 语言的方式能够极大地减少函数调用的额外开销,但是也增加了实现的复杂度;
      • CPU 访问栈的开销比访问寄存器高几十倍;
      • 需要单独处理函数参数过多的情况;
    • Go 语言的方式能够降低实现的复杂度并支持多返回值,但是牺牲了函数调用的性能;
      • 不需要考虑超过寄存器数量的参数应该如何传递;
      • 不需要考虑不同架构上的寄存器差异;
      • 函数入参和出参的内存空间需要在栈上进行分配;

var

没有预分配内存,会产生多个内存分配,如果 append 的次数极多,则会产生大量的内存分配,是低效的

var s []int
s = append(s, 0, 1, 2, 3, 4)
fmt.Println(s, len(s), cap(s))

var s = []int{0, 1, 2, 3, 4}
fmt.Println(s, len(s), cap(s))

或使用 : 省略 var

s := []int{0, 1, 2, 3, 4}
fmt.Println(s, len(s), cap(s))

或,提前进行内存分配,假如有 10000 个数字将要 append 进切片,则提前内存分配会省去内存分配 10000 次的性能损耗 make([]int, 0, 10000)

s := make([]int, 0, 5)
s = append(s, 0, 1, 2, 3, 4)
fmt.Println(s, len(s), cap(s))

结果

[0 1 2 3 4] 5 6
[0 1 2 3 4] 5 5
[0 1 2 3 4] 5 5
[0 1 2 3 4] 5 6

变量申明总结

方式

var foo int = 200
// 类型推断
var foo = 200
var foo, foo2, foo3 int
var foo, foo2, foo3 int = 200, 300, 400
// 类型推断
var foo, foo2, foo3 = 200, 300.30, true
var(
foo = 200
foo2 = 300.30
foo3 bool
foo4 = "Foo4"
foo5 string = "Foo5"
)

变量名规则

  • 当声明一个没有初始化的变量时,使用var语法
  • 在声明和显式初始化变量时,使用:=
  • 区分大小写:任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头
  • 循环和分支使用单字母变量,参数和返回值使用单词,函数和包级声明使用多词
  • 方法、接口和包最好使用单个词
  • 不要在变量名称中包含类型名称,例如修改var usersMap map[string]*Uservar users map[string]*User
  • 建议使用length := uint32(0x80)代替var length uint32 = 0x80,用来暗示事情很复杂,而不是遵循第一条规则
  • 使用thing := &Thing{}thing := Thing()代替thing := new(Thing),避免关键字的不常见用法
  • 使用统一的风格

使用min, max := 0, 1000代替

var min int
max := 1000
  • 不要让包名窃取好的变量名

导入标识符的名称(Content)包括其包名称(content),例如包 content 中的类型 Content 将被称为context.Context,这使得context无法在包中用作变量或类型

func WriteLog(context context.Context, message string)

上方代码不会编译。这就是为什么context.Context类型的本地声明传统上是ctx

func WriteLog(ctx context.Context, message string)

包名称规则

  • 为您的包命名它提供的内容,而不是它包含的内容
  • 避免使用包名称,如base``commonutil
  • 包中的Get函数在被另一个包引用net/http时变为http.Get
  • 当导入其他包时,strings包中的Reader类型变为strings.Reader
  • 避免包级状态

数字

int 整数

  • int 在 32 位系统代表 int32,在 64 位系统代表 int64
  • int8 范围 -128 ~ 127 math.MinInt8 ~ math.MaxInt8
  • int16 范围 -32768 ~ 32767
  • int32 范围 -2147483648 ~ 2147483647=-(2^32)/2-1 ~ (2^32)/2-1
  • int64 范围 -9223372036854775808 ~ 9223372036854775807

uint 无符号版

  • uint
  • uint8 范围 0 ~ 255
  • uint16 范围 0 ~ 65535
  • uint32 范围 0 ~ 4294967295
  • uint64 范围 0 ~ 18446744073709551615
  • uintptr 一个大到足以存储指针值的未解释位的无符号整数,使用 unitptr 当需要对内存地址进行加减操作时(go 不会将它视为指针),从而当作数字来进行运算

byte 范围 0 ~ 255 uint8 的别称
rune int32 的别称
float 小数

  • float32范围 1.4e-45 ~ 3.4e38 math.MinFloat32 ~math.MaxFloat32
  • float64范围 4.9e-324 ~ 1.8e308math.MinFloat64 ~math.MaxFloat64

complex 复数

  • complex64
  • complex128

注意:

  • int8 转 byte 使用byte(-70)=186byte(256 + int(-70))(<0 时)
  • int、uint 和 uintptr 类型在 32 位系统代表 32 位,在 64 位系统代表 64 位,除byterune所有数字类型都是不同的,相互运算需要转换类型,包括 32 位系统时的intint32也需要转换

字符串

  • 字符串的元素不可修改,是一个只读的字节数组,所以字符串拼接时会产生临时字符串
  • 字符串其实是一个结构体,因此字符串的赋值操作也就是 reflect.StringHeader 结构体的复制过程,并不会涉及底层字节数组的复制
  • Hello, world字符串底层数据和以下数组是完全一致
var data = [...]byte{
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd',
}
  • 字符串虽然不是切片,但是支持切片操作
s := "hello, world"
hello := s[:5]
world := s[7:]
  • 遍历字符串
    • 故意损坏了第一字符的第二和第三字节,for range 迭代这个含有损坏的 UTF8 字符串时,第一字符的第二和第三字节依然会被单独迭代到
for i, c := range "\xe4\x00\x00\xe7\x95\x8cabc" {
fmt.Println(i, c)
}
// 0 65533 // \uFFFD, 对应 �
// 1 0 // 空字符
// 2 0 // 空字符
// 3 30028 // 界
// 6 97 // a
// 7 98 // b
// 8 99 // c
  • []byte 转为 string,使用string([]byte)
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
fmt.Println(string(b))

结果

[123 34 78 97 109 101 34 58 34 87 101 100 110 101 115 100 97 121 34 44 34 65 103 101 34 58 54 44 34 80 97 114 101 110 116 115 34 58 91 34 71 111 109 101 122 34 44 34 77 111 114 116 105 99 105 97 34 93 125]
{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}

字符串拼接

package main

import (
"bytes"
"fmt"
"strings"
"testing"
)

func BenchmarkAddStringWithOperator(b *testing.B) {
hello := "hello"
world := "world"
for i := 0; i < b.N; i++ {
_ = hello + "," + world
}
}

func BenchmarkAddStringWithSprintf(b *testing.B) {
hello := "hello"
world := "world"
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%s,%s", hello, world)
}
}

func BenchmarkAddStringWithJoin(b *testing.B) {
// hello := "hello"
// world := "world"
for i := 0; i < b.N; i++ {
_ = strings.Join([]string{"hello", "world"}, ",")
}
}

func BenchmarkAddStringWithBuffer(b *testing.B) {
hello := "hello"
world := "world"
for i := 0; i < b.N; i++ {
var buffer bytes.Buffer
buffer.WriteString(hello)
buffer.WriteString(",")
buffer.WriteString(world)
_ = buffer.String()
}
}

结果

BenchmarkAddStringWithOperator-4   	28661848	        43.38 ns/op	       0 B/op	       0 allocs/op
BenchmarkAddStringWithSprintf-4 3955856 253.5 ns/op 48 B/op 3 allocs/op
BenchmarkAddStringWithJoin-4 20002099 61.55 ns/op 16 B/op 1 allocs/op
BenchmarkAddStringWithBuffer-4 13969130 98.88 ns/op 64 B/op 1 allocs/op

可见

  • 使用 + 运算符性能最高,且无内存分配(0 allocs/op ),每次分配 0 字节(0 B/op)(推荐)
  • 使用 join 性能次之,不过需要字符串数组
  • 使用 Sprintf 性能最低,分配内存的次数等于变量的个数+1

string 和[]byte

string 的定义

type stringStruct struct {
str unsafe.Pointer
len int
}
  • 可以看到 string 确实是个指向数组首地址的指针
  • 字符串的操作尽量用 []byte 来做,bytes 包支持的功能和 strings 包的差不多。string 值不可修改,所以每次修改时会创建一个新的 string,要想原地修改就只能用 []byte ,如果不涉及修改我们的数据,最好传递字符串,高效又安全
  • byte 其实只不过是 uint8(无符号版 0 ~ 255) 的一个别名,两者之间任何时候都是互相等价的,[]byte是 byte 类型的数组
  • string 值不可为 nil,所以如果你想要通过返回 nil 表达点什么额外的含义,就只能用[]byte
  • []byte可以是数组也可以是切片,Go 语言的切片这么灵活,想要用切片的特性就只能用[]byte
  • 如果你要调用第三方库,如果第三方库主要用的是 string,那么为了避免频繁的进行类型转换,那你就可以用 string。毕竟 Go 是强类型语言,类型转换会极大地降低代码的可读性。[]byte也是同理。
  • 需要进行字符串处理的时候,因为 []byte 支持切片,并且可以原地修改,所以 string 更快一些,所以注重性能的地方你可以用[]byte
  • 定义一个 json 格式的[]byte字符串数组
newfoojson := []byte(`{"Ptr1":"Ptr1val","Ptr2":"Ptr2val"}`)

复杂结构体

定义

type Foo struct {
Ptr1 string
Ptr2 string
test string
}

新申明一个复杂接口体类型的变量

foo := Foo{
Ptr1: "Ptr1val",
Ptr2: "Ptr2val",
}

数组

数组声明

arr1 := [3]int{1, 2, 3}
arr2 := [...]int{1, 2, 3}

后一种声明方式在编译期间就会被转换成前一种
[4]int 的内存表示只是四个按顺序排列的整数值

slice

数组和切片都不是并发安全的
结构体 slice

type slice struct {
array unsafe.Pointer // 指向数组的指针
len int // 长度
cap int // 容量
}
  • 切片是数组段的描述符,由以下三项组成
    • 一个指向数组的指针,所以要小心函数内修改,可能会影响外层
    • slice 的长度
    • 它的容量(slice 的最大长度)
  • 由切片生成切片时:
    • 新切片由旧切片复制而来,但由切片的结构体可见并不会复制切片的数据,只是复制指向切片数据的指针,性能很高
    • 新切片扩容前,由于新旧切片公用一个内存地址,对新切片的操作均会同步到旧切片(如排序、下标定位修改s[1] = 2等),但 append 这种修改长度的不会生效到旧切片,因为旧切片的 len 和 cap 是存储在结构体中
    • 新切片发生扩容时,生成新的内存地址,此后所有对新切片的操作均不会对旧切片产生影响,因为内存地址不共用
  • slice 作为函数传参时,复制的是整个结构体(内部的指针长度容量的值均不变),新旧操作影响原理同由切片生成切片
  • 由数组生成切片时:
    • 新生成的切片不会复制数组的数据,它创建一个指向原始数组的新切片值。这使得切片操作与操作数组索引一样有效,因此,修改新切片的元素会修改原始数组的元素
    • 新切片扩容后进行的修改将不会影响原始数组
func main() {
s := [5]int{0, 1, 2, 3, 4}

fmt.Printf("数组初始值 %v\n", s)
a := s[1:]
fmt.Printf("通过数组生成切片 %v, cap %v, len %v, %p\n", a, cap(a), len(a), a)
a[1] = 3
fmt.Printf("修改切片后切片值 %v, cap %v, len %v, %p\n", a, cap(a), len(a), a)
fmt.Printf("修改切片后数组值 %v\n", s)

a = append(a, 999)
fmt.Printf("切片append后切片值 %v, cap %v, len %v, %p\n", a, cap(a), len(a), a)
fmt.Printf("切片append后数组值 %v\n", s)

a[1] = 900
fmt.Printf("切片扩容后修改时切片值 %v, cap %v, len %v, %p\n", a, cap(a), len(a), a)
fmt.Printf("切片扩容后修改时数组值 %v\n", s)
}
数组初始值 [0 1 2 3 4]
通过数组生成切片 [1 2 3 4], cap 4, len 4, 0xc00000c3c8
修改切片后切片值 [1 3 3 4], cap 4, len 4, 0xc00000c3c8
修改切片后数组值 [0 1 3 3 4]
切片append后切片值 [1 3 3 4 999], cap 8, len 5, 0xc00000e280
切片append后数组值 [0 1 3 3 4]
切片扩容后修改时切片值 [1 900 3 4 999], cap 8, len 5, 0xc00000e280
切片扩容后修改时数组值 [0 1 3 3 4]
  • 通过func makeslice(et *_type, len, cap int) slice 创建,函数返回的是 Slice 结构体,所以当 map 和 slice 作为函数参数时,在函数参数内部对 map 的操作会影响 map 自身;而对于 slice 却不一定会
    • slice 作为入参,函数内 append 不会影响外部
    • slice 作为入参,切片扩容前会体现到函数外部,扩容后的修改不会体现到函数外部
  • 预先分配内存可以提升性能,使用 index 赋值比 append 赋值性能更高
  • slice 当前容量小于 1024 时(cap<1024),每次扩容是成倍增长,当 cap>=1024 时,每次*1.25,见下方的例子
func main() {
s := make([]int, 0)
for i := 0; i < 5; i++ {
s = append(s, i)
fmt.Printf("cap %v, len %v, %p\n", cap(s), len(s), s)
}

a := make([]int, 0, 3)
for i := 0; i < 5; i++ {
a = append(a, i)
fmt.Printf("cap %v, len %v, %p\n", cap(a), len(a), a)
}
}

结果

cap 1, len 1, 0xc000128058
cap 2, len 2, 0xc0001280a0
cap 4, len 3, 0xc000126080
cap 4, len 4, 0xc000126080
cap 8, len 5, 0xc0001420c0

cap 3, len 1, 0xc00012a090
cap 3, len 2, 0xc00012a090
cap 3, len 3, 0xc00012a090
cap 6, len 4, 0xc00013e060
cap 6, len 5, 0xc00013e060
  • slice 单个 append 时,cap 是成倍或 1.25 倍增长,当一次 append 多个时,go 会进行内存优化,见下方的例子
func main() {
s := make([]int, 0)
s = append(s, 0, 1, 2)
fmt.Printf("cap %v, len %v, %p\n", cap(s), len(s), s)
}

结果

cap 3, len 3, 0xc0000ae090

定义 slice

  1. 通过 var
var s []int
s = append(s, 0, 1, 2, 3, 4)
// 显示长度
fmt.Println(len(s))
// 显示容量
fmt.Println(cap(s))
  1. 或通过 make 函数,make 分配一个数组并返回一个引用该数组的切片
s := make([]int, 5, 5)
// s == []int{0, 0, 0, 0, 0}
s = append(s, 0, 1, 2, 3, 4)
// s == []int{0 0 0 0 0 0 1 2 3 4}
  1. 或通过切片现有切片或数组形成(通过[start:end]),返回新的从 start(从 0 开始)开始到 end 结束但不包括 end 的指针,新切片索引从 0 开始,对新切片的值修改会同步到旧切片,长度修改(如a = append(a, 4))不会同步到旧切片
a := s[1:2]
// a == []int{1}
fmt.Println(a[0])
// 1
func main() {
s := make([]int, 0, 2)
for i := 0; i < 3; i++ {
s = append(s, i)
}

fmt.Println("初始值:")
fmt.Printf("旧切片值 %v, cap %v, len %v, %p\n", s, cap(s), len(s), s)
a := s[1:]
fmt.Printf("新切片值 %v, cap %v, len %v, %p\n", a, cap(a), len(a), a)

a[1] = 3
fmt.Println("修改新切片时:")
fmt.Printf("旧切片值 %v, cap %v, len %v, %p\n", s, cap(s), len(s), s)
fmt.Printf("新切片值 %v, cap %v, len %v, %p\n", a, cap(a), len(a), a)

a = append(a, 4)
fmt.Println("新切片不扩容时:")
fmt.Printf("旧切片值 %v, cap %v, len %v, %p\n", s, cap(s), len(s), s)
fmt.Printf("新切片值 %v, cap %v, len %v, %p\n", a, cap(a), len(a), a)

a = append(a, 4)
fmt.Println("新切片扩容时:")
fmt.Printf("旧切片值 %v, cap %v, len %v, %p\n", s, cap(s), len(s), s)
fmt.Printf("新切片值 %v, cap %v, len %v, %p\n", a, cap(a), len(a), a)

a[2] = 5
fmt.Println("扩容后修改时:")
fmt.Printf("旧切片值 %v, cap %v, len %v, %p\n", s, cap(s), len(s), s)
fmt.Printf("新切片值 %v, cap %v, len %v, %p\n", a, cap(a), len(a), a)
}
初始值:
旧切片值 [0 1 2], cap 4, len 3, 0xc000010260
新切片值 [1 2], cap 3, len 2, 0xc000010268
修改新切片时:
旧切片值 [0 1 3], cap 4, len 3, 0xc000010260
新切片值 [1 3], cap 3, len 2, 0xc000010268
新切片不扩容时:
旧切片值 [0 1 3], cap 4, len 3, 0xc000010260
新切片值 [1 3 4], cap 3, len 3, 0xc000010268
新切片扩容时:
旧切片值 [0 1 3], cap 4, len 3, 0xc000010260
新切片值 [1 3 4 4], cap 6, len 4, 0xc00000c3c0
扩容后修改时:
旧切片值 [0 1 3], cap 4, len 3, 0xc000010260
新切片值 [1 3 5 4], cap 6, len 4, 0xc00000c3c0
  • 旧切片 s 的内存地址从始至终不会发生变化
  • 新切片 a 的内存地址开始未变化,当发生扩容时内存地址发生变化(0xc000010268 -> 0xc00000c3c0),很明显地址变化后所有对 a 的操作对 s 均不会生效
  • 新切片 a 发生扩容前进行排序或值变化均会影响旧切片 s,由于旧切片结构体中的 len 和 cap 均不变,所以,对 a 进行 append 不会对 s 生效,s 始终只取前三位
  1. 将一个切片附加到另一个切片
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // append(a, b[0], b[1], b[2])
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

或使用 copy 复制切片

func copy(dst, src []T) int

copy 函数支持在不同长度的切片之间进行复制(它只会复制到较少数量的元素)

s := []int{0, 1, 2, 3, 4}
fmt.Println("s", s, len(s), cap(s))

b := s[1:3]
fmt.Println("b", b, len(b), cap(b))

a := s[3:4]
fmt.Println("a", a, len(a), cap(a))

copy(a, b)
fmt.Println("a", a, len(a), cap(a))

结果

s [0 1 2 3 4] 5 5
b [1 2] 2 4
a [3] 1 2
a [1] 1 2
  1. var s3 []int 声明的值为 nil,s := []int{}s2 := make([]int, 0) 声明的值为空数组
func main() {
s := []int{}
b, _ := json.Marshal(s)
fmt.Println(string(b))

s2 := make([]int, 0)
b2, _ := json.Marshal(s2)
fmt.Println(string(b2))

var s3 []int
b3, _ := json.Marshal(s3)
fmt.Println(string(b3))
}

结果

[]
[]
null

定义 array

var a [length]int

数组的长度是在编译时静态计算的,且无法在运行时动态扩缩容

不同申明 slice 方式的效率

func BenchmarkAppend(b *testing.B) {
for i := 0; i < b.N; i++ {
var s []int
for j := 0; j < 10000; j++ {
s = append(s, j)
}
}
}

func BenchmarkAppendAllocated(b *testing.B) {
for i := 0; i < b.N; i++ {
s := make([]int, 0, 10000)
for j := 0; j < 10000; j++ {
s = append(s, j)
}
}
}

func BenchmarkAppendIndexed(b *testing.B) {
for i := 0; i < b.N; i++ {
s := make([]int, 10000)
for j := 0; j < 10000; j++ {
s[j] = j
}
}
}

详见: Go test

函数中对 slice 进行修改

package main

import "fmt"

func main() {
testSlice1()
testSlice2()
testSlice3()
testSlice4()
}

func testSlice1() {
var s []int
for i := 0; i < 3; i++ {
s = append(s, i)
}
modifySlice1(s)

fmt.Println(s)
}
func testSlice2() {
var s []int
for i := 0; i < 3; i++ {
s = append(s, i)
}
modifySlice2(s)

fmt.Println(s)
}
func testSlice3() {
var s []int
for i := 0; i < 3; i++ {
s = append(s, i)
}
modifySlice3(s)

fmt.Println(s)
}
func testSlice4() {
var s []int
for i := 0; i < 3; i++ {
s = append(s, i)
}
modifySlice4(s)

fmt.Println(s)
}

func modifySlice1(s []int) {
s[0] = 1024
}
func modifySlice2(s []int) {
s = append(s, 2048)
s[0] = 1024
}
func modifySlice3(s []int) {
s = append(s, 2048)
s = append(s, 4096)
s[0] = 1024
}
func modifySlice4(s []int) {
s[0] = 1024
s = append(s, 2048)
s = append(s, 4096)
}

结果:

[1024 1 2]
[1024 1 2]
[0 1 2]
[1024 1 2]
  • modifySlice1 因为切片内部是指向具体数组的指针,所以函数内操作会在函数外部testSlice1 体现
  • modifySlice2 由于 s 的 cap 为 4,第一次 append 时不会发生扩容,对新切片的修改会同步到外层旧切片,但长度变化不会生效到旧切片,所以旧切片变为了1024,但没有2048
  • modifySlice3 由于 s 的 cap 为 4,第二次 append 时会发生扩容,函数 modifySolice3 内部使用的 s 和函数外部testSlice3 使用的 s 所使用的存储空间是完全不一样的,所以两次 append 和索引修改 s 都是在新内存地址上的修改,并不会体现到函数外部的 s 上
  • modifySlice4 由于先使用了索引方式修改,已经生效到外部函数,所以结果是 [1024 1 2]

警惕切片指向的数组一直占用内存

重新切片不会复制底层数组。完整数组将保存在内存中,直到不再被引用。
例如,将一个文件加载到内存中,并在其中搜索第一组连续数字,并将它们作为新切片返回

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}

返回的[]byte 指向包含整个文件的数组。由于切片引用了原始数组,所以只要切片保持在垃圾收集器周围,就无法释放数组;文件的几个有用字节将使整个内容保存在内存中,通过将数据复制到一个新的切片解决

func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}

边界检查优化 Bounds Check Elimination(bce)

func normal(s []int) {
i := 0
i += s[0]
i += s[1]
i += s[2]
i += s[3]
fmt.Println(i)
}

func bce(s []int) {
_ = s[3]

i := 0
i += s[0]
i += s[1]
i += s[2]
i += s[3]
fmt.Println(i)
}

如果能确定访问到的 slice 的长度,可以先执行一次访问,让编译器去优化,防止每次下标检查耗费性能

map

map 本质上不是一个 hmap 的结构体,实际上是指向 hmap 结构体的一个指针

m := make(map[int]int)
modifyMap(m)

实际伪代码:

var m *hmap = &hmap{...}
modifyMap(m)

结构体 hmap

type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
type mapextra struct {
// overflow[0] contains overflow buckets for hmap.buckets.
// overflow[1] contains overflow buckets for hmap.oldbuckets.
overflow [2]*[]*bmap

// nextOverflow 包含空闲的 overflow bucket,这是预分配的 bucket
nextOverflow *bmap
}
type hmap struct {
// 元素个数,调用 len(map) 时,直接返回此值
count int
flags uint8
// buckets 的对数 log_2
B uint8
// overflow 的 bucket 近似数
noverflow uint16
// 计算 key 的哈希的时候会传入哈希函数
hash0 uint32
// 指向 buckets 数组,大小为 2^B
// 如果元素个数为0,就为 nil
buckets unsafe.Pointer
// 扩容的时候,buckets 长度会是 oldbuckets 的两倍
oldbuckets unsafe.Pointer
// 指示扩容进度,小于此地址的 buckets 迁移完成
nevacuate uintptr
extra *mapextra // optional fields
}
  • map 解决碰撞使用的是拉链法,和 php 一样:PHP 常用#hashTable
  • map 赋值时会自动扩容,map 删除 key 不会自动缩容释放内存,防止快速删除大量 key,又快速增加大量 key 带来的扩缩容消耗,可手动使用 runtime.GC() 清理内存,所以禁止在全局变量中使用 map,导致内存占用居高不下
  • 哈希函数:如果支持 aes,则使用 aes hash,否则使用 memhash
  • buckets 桶是个数组,数组的长度=2^B,如果 B = 5,则代表有 32 个桶,当两个不同的 key 落在同一个桶中,也就是发生了哈希冲突
  • 通过func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap 创建 map,函数返回的是*hmap 指针,所以当 map 和 slice 作为函数参数时,在函数参数内部对 map 的操作会影响 map 自身;而对 slice 却不会(slice 返回的是)
  • map 的 key value 都不可取地址,会随着扩容地址发生改变
  • 只有在更新 map 的时候,map 才是并发不安全的,全部是读操作的并发是安全的。Go 1.6 对 map 的并发读写进行了更明确的规定:当一个协程正在对 map 进行写操作的时候,不能有其它协程在对同一个 map 进行操作,读和写都不行。Go 的 runtime 会对 map 的并发读写进行监测,如果发现不安全的操作直接 crash

详见:深度解密 Go 语言之 map

循环 map

m := make(map[int]int)
for k := range m {
delete(m, k)
}

Goroutine

轻量级线程
Goroutines 相对于线程的优势

  • 与线程相比,Goroutines 非常便宜。它们的堆栈大小只有 2 kb,堆栈可以根据应用程序的需要增长和缩小,而在线程的情况下,堆栈大小必须指定并固定。
  • Goroutine 被多路复用到更少数量的 OS 线程。一个包含数千个 Goroutine 的程序中可能只有一个线程。如果该线程中的任何 Goroutine 阻塞等待用户输入,则创建另一个 OS 线程并将剩余的 Goroutine 移动到新的 OS 线程。所有这些都由运行时处理,我们作为程序员从这些复杂的细节中抽象出来,并获得了一个干净的 API 来处理并发性。
  • Goroutines 使用 Channel 进行通信。Channel 通过设计防止在使用 Goroutine 访问共享内存时发生竞争条件。通道可以被认为是 Goroutine 进行通信的管道。

详见:Goroutines vs 多线程(转)

Channel 管道

  • 发消息ch <- 0
  • 取走消息<- ch
  • channel 是有锁的,所以不适用于高并发高性能编程,不应该用来大量数据的传递,可用来通知
  • channel 会触发调度
  • unbuffered Channel:无缓冲区的 channel,只会发生一次 copy(send goroutine -> receive goroutine)发送方会直接将数据交给接收方;且 receive 完成后 send 才返回
  • buffered Channel:有缓冲区的 channel,会发生两次 copy(send goroutine -> buf; buf -> receive goroutine)基于环形缓存的传统生产者消费者模型;
  • 所以短时间的通知使用 unbuffered channel 性能更好(只发生一次 copy)
  • chan struct{} 类型的异步 Channel — struct{} 类型不占用内存空间,不需要实现缓冲区和直接发送(Handoff)的语义;
  • time.Sleep() 可以让出 cpu
  • closedchannel执行:
    • read可继续读取剩余数据
    • write会引起 panic
    • 判断是否已关闭
      • value, ok := <- chok 是 false 就代表已关闭
      • for value := range ch {}如果 channel 被关闭会跳出循环
  • 通过Channelsync.WaitGroup可以解决下列示例中go loop()还未执行,main已经退出的问题
func loop() {
for i := 0; i < ; i++ {
fmt.Printf("%d ", i)
}
}
func main() {
go loop()
loop()
}

结果

0 1 2 3 4 5 6 7 8 9

通过Channel解决

var complete chan int = make(chan int)

func loop() {
for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}

complete <- 0 // 执行完毕了,发个消息
}


func main() {
go loop()
<- complete // 直到线程跑完, 取到消息. main在此阻塞住
}

通过sync.WaitGroup解决

func main() {
var wg sync.WaitGroup

wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}
}()
wg.Wait()

for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}
}

无缓冲区

  • 无缓冲的信道永远不会存储数据,只负责数据的流通
    • 从无缓冲信道取数据,必须要有数据流进来才可以,否则当前线阻塞
    • 数据流入无缓冲信道, 如果没有其他 goroutine 来拿走这个数据,那么当前线阻塞

示例

var ch chan int = make(chan int)
// 或
// ch := make(chan int)

func foo() {
ch <- 0 // 向ch中加数据,如果没有其他goroutine来取走这个数据,那么挂起foo, 直到main函数把0这个数据拿走
}

func main() {
go foo()
<- ch // 从ch取数据,如果ch中还没放数据,那就挂起main线,直到foo函数中放数据为止
}

交替打印奇数和偶数示例

var ch = make(chan struct{})
var wg sync.WaitGroup

func go1() {
defer wg.Done()
for i := 1; i <= 10; i += 2 {
fmt.Printf("go1 %v\n", i)
ch <- struct{}{} //不能与上一行交换位置
<-ch
}
}
func go2() {
defer wg.Done()
for i := 2; i <= 10; i += 2 {
<-ch
fmt.Printf("go2 %v\n", i)
ch <- struct{}{}
}
}
func main() {
wg.Add(2)
go go1()
go go2()
wg.Wait()
}

结果

go1 1
go2 2
go1 3
go2 4
go1 5
go2 6
go1 7
go2 8
go1 9
go2 10

有缓冲区

缓冲信道不仅可以流通数据,还可以缓存数据

sync

WaitGroup

一个 WaitGroup 对象可以等待一组协程结束

func main() {
tasks := []func(){
func() { fmt.Printf("1. \n") },
func() { fmt.Printf("2. \n") },
func() { fmt.Printf("3. \n") },
func() { fmt.Printf("4. \n") },
}

var wg sync.WaitGroup
for key, task := range tasks {
fmt.Printf("task: %p %p %v\n", &task, &key, key)
i := key
fmt.Printf("i: %p %v\n", &i, i)

wg.Add(1)
go func() {
fmt.Printf("task: %p %p %v\n", &task, &key, key)
task()
wg.Done()
}()
wg.Wait()
}
}

结果

task: 0xc000006028 0xc000014098 0
i: 0xc0000140c8 0
task: 0xc000006028 0xc000014098 0
1.
task: 0xc000006028 0xc000014098 1
i: 0xc0000140e0 1
task: 0xc000006028 0xc000014098 1
2.
task: 0xc000006028 0xc000014098 2
i: 0xc0000140e8 2
task: 0xc000006028 0xc000014098 2
3.
task: 0xc000006028 0xc000014098 3
i: 0xc0000140f0 3
task: 0xc000006028 0xc000014098 3
4.
  • 通过调用 wg.Add(delta int) 设置协程的个数,最终形成一组协程,一组协程的总数=delta 之和
  • worker 协程执行结束以后,都要调用 wg.Done()
  • 调用 wg.Wait() 且被阻塞,直到所有 worker 协程全部执行结束后继续

很明显这种写法并不是并发执行四次打印,下方的写法才是:

func main() {
tasks := []func(){
func() { fmt.Printf("1. \n") },
func() { fmt.Printf("2. \n") },
func() { fmt.Printf("3. \n") },
func() { fmt.Printf("4. \n") },
}

var wg sync.WaitGroup
wg.Add(len(tasks))
for key, task := range tasks {
fmt.Printf("task: %p %p %v\n", &task, &key, key)
i := key
fmt.Printf("i: %p %v\n", &i, i)

// 必须,否则IDE提示 loop variable task captured by func literal
task := task
key := key
go func() {
defer wg.Done()
fmt.Printf("task go: %p %p %v\n", &task, &key, key)
task()
}()
}
wg.Wait()
}

结果

task: 0xc000006028 0xc000014098 0
i: 0xc0000140c8 0
task: 0xc000006028 0xc000014098 1
i: 0xc0000140e8 1
task: 0xc000006028 0xc000014098 2
i: 0xc0000140f8 2
task: 0xc000006028 0xc000014098 3
i: 0xc000014108 3
task go: 0xc000006038 0xc0000140e0 0
1.
task go: 0xc000006048 0xc000014100 2
3.
task go: 0xc000006050 0xc000014110 3
4.
task go: 0xc000006040 0xc0000140f0 1
2.
  • 不使用 task := task 不为 task 申请新内存时,将提示loop variable task captured by func literal,且全部打印4.,如下:
task: 0xc000006028 0xc000014098 0
i: 0xc0000140c8 0
task: 0xc000006028 0xc000014098 1
i: 0xc0000140e0 1
task: 0xc000006028 0xc000014098 2
i: 0xc0000140e8 2
task: 0xc000006028 0xc000014098 3
i: 0xc0000140f0 3
task go: 0xc000006028 0xc000014098 3
4.
task go: 0xc000006028 0xc000014098 3
4.
task go: 0xc000006028 0xc000014098 3
4.
task go: 0xc000006028 0xc000014098 3
4.

Mutex

锁的特点就是慢
解决慢的方法

  • 减小锁的粒度-使用分段锁,分段锁是把 map 分成了多个小份(submap),每个小份使用独立的锁,从而很大概率上避免了锁竞争,只有对同一小份 map(submap)进行操作时才会产生锁竞争,代码见Go 分段锁
  • 缩小临界区,即尽可能早的调用m.Unlock(),此时由于不使用 defer 来调用m.Unlock(),要注意中间不要有 return 之类导致死锁的跳出语句
  • 读写分离,使用sync.RWMutex

指针

切片复制时的注意事项在 slice 部分已经详细介绍,其实不仅限于切片,任何带有指针的类型都可能受到影响,见下方示例

type A struct {
Ptr1 *B
Ptr2 *B
Val B
}

type B struct {
Str string
}

func main() {
a := A{
Ptr1: &B{"ptr-str-1"},
Ptr2: &B{"ptr-str-2"},
Val: B{"val-str"},
}
fmt.Printf("Ptr1: %v%p, Ptr2: %v%p, Val: %v\n", a.Ptr1, a.Ptr1, a.Ptr2, a.Ptr2, a.Val)
demo(a)
fmt.Printf("Ptr1: %v%p, Ptr2: %v%p, Val: %v\n", a.Ptr1, a.Ptr1, a.Ptr2, a.Ptr2, a.Val)
}

func demo(a A) {
a.Ptr1.Str = "new-ptr-str1"
a.Ptr2 = &B{"new-ptr-str-2"}
a.Val.Str = "new-val-str"

fmt.Printf("Ptr1: %v%p, Ptr2: %v%p, Val: %v\n", a.Ptr1, a.Ptr1, a.Ptr2, a.Ptr2, a.Val)
}

结果

Ptr1: &{ptr-str-1}0xc00003a240, Ptr2: &{ptr-str-2}0xc00003a250, Val: {val-str}
Ptr1: &{new-ptr-str1}0xc00003a240, Ptr2: &{new-ptr-str-2}0xc00003a2b0, Val: {new-val-str}
Ptr1: &{new-ptr-str1}0xc00003a240, Ptr2: &{ptr-str-2}0xc00003a250, Val: {val-str}

可见由带指针类型的结构体复制生成新结构体时:

  • 当修改指针对应的内存数据的值时 a.Ptr1.Str = "new-ptr-str1",所有指向此内存的指针均会受影响(包括旧结构体和复制后的新结构体)
  • 当修改指针的指向时 a.Ptr2 = &B{"new-ptr-str-2"},不会对旧结构体生效

Json

反序列化

定义 Foo 类型,定义 MyRawMessage 类型反解析时自定义处理

type Foo struct {
Ptr1 string
Ptr2 string
test string
}

type MyRawMessage []byte

// UnmarshalJSON sets *m to a copy of data.
func (m *MyRawMessage) UnmarshalJSON(data []byte) error {
fmt.Println("UnmarshalJSON")
if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
*m = append((*m)[0:0], data...)
return nil
}

func main() {
// []byte反序列化为go结构体
newfoojson := []byte(`{"Ptr1":"Ptr1val","Ptr2":"Ptr2val","test":"aaa"}`)

var foores MyRawMessage
json.Unmarshal(newfoojson, &foores)
fmt.Printf("%T: %v %T: %v\n", newfoojson, string(newfoojson), foores, string(foores))
}

结果

UnmarshalJSON
[]uint8: {"Ptr1":"Ptr1val","Ptr2":"Ptr2val","test":"aaa"} main.MyRawMessage: {"Ptr1":"Ptr1val","Ptr2":"Ptr2val","test":"aaa"}

参照接口实现示例:src/encoding/json/stream.go

type RawMessage []byte

// MarshalJSON returns m as the JSON encoding of m.
func (m RawMessage) MarshalJSON() ([]byte, error) {
if m == nil {
return []byte("null"), nil
}
return m, nil
}

// UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
*m = append((*m)[0:0], data...)
return nil
}

Unmarshaler 接口定义:src/encoding/json/decode.go

type Unmarshaler interface {
UnmarshalJSON([]byte) error
}

Marshaler 接口定义:src/encoding/json/encode.go

type Marshaler interface {
MarshalJSON() ([]byte, error)
}

序列化

自定义结构体JavaTime序列化时的值为0

// JavaTime(time.Time) <==> time.Time(JavaTime) 互转
type JavaTime time.Time

// UnmarshalJSON : UnmarshalJSON 自定义从json->转换器
func (j *JavaTime) UnmarshalJSON(data []byte) error {
fmt.Println("UnmarshalJSON")
millis, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
*j = JavaTime(time.Unix(0, millis*int64(time.Millisecond)))
return nil
}

// MarshalJSON : 自定义对象转换到 json
func (j *JavaTime) MarshalJSON() (data []byte, err error) {
fmt.Println("MarshalJSON")
var buf bytes.Buffer

// 使用入参时间
// origin := time.Time(*j)

// 使用当前时间
// buf.WriteString(strconv.FormatInt(time.Now().UnixNano()/int64(time.Microsecond), 10))

// 使用字符串 0
buf.WriteString(strconv.FormatInt(0, 10))

return buf.Bytes(), nil
}

调用

package main

import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"time"
)

// Person : for test struct
type Person struct {
Birthday *JavaTime `json:"birthday"`
}

func main() {
bir := JavaTime(time.Date(1991, time.August, 01, 0, 0, 0, 0, time.UTC))
p := Person{Birthday: &bir}

// 从 Person 类型转换为 json 字符串:
j, _ := json.Marshal(p)
fmt.Printf("json := %v\n", string(j))

// 从 json 字符串,转换为 Person 类型:
json.Unmarshal(j, &p)

bir = *p.Birthday
golangTime := time.Time(bir)
fmt.Printf("bir year = %v\n", golangTime.Year())
}

结果

MarshalJSON
json := {"birthday":0}
UnmarshalJSON
bir year = 1970

由于时间 json encode 时被设置为 0,故 decode 时 year = 1970

encode:入参接收器返回[]byte 字符串
decode:入参[]byte 字符串返回到接收器

Print

  • Printf 谓词语法
    • %s 打印字符串
    • %v 打印值
    • %#v 打印详细信息
fmt.Printf("%#v\n", []byte("Hello, 世界"))
[]byte{0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c}

0xe4, 0xb8, 0x96 对应中文0xe7, 0x95, 0x8c 对应中文

  • %T 打印类型
fmt.Printf("%T\n", []byte("Hello, 世界"))
[]byte
  • %p 打印指针地址
type A struct {
Ptr1 *B
Ptr2 *B
Val B
}

type B struct {
Str string
}

func main() {
a := A{
Ptr1: &B{"ptr-str-1"},
Ptr2: &B{"ptr-str-2"},
Val: B{"val-str"},
}

fmt.Printf("Ptr1: %v%p\n", a.Ptr1, a.Ptr1)
fmt.Printf("Ptr1: %v%p\n", a.Val, &a.Val)
}

指针类型可直接使用 %p 打印,非指针类型使用 & 取地址打印
结果

Ptr1: &{ptr-str-1}0xc00003a240
Ptr1: {val-str}0xc0000243f0
  • Println 打印并换行
  • 打印内存使用情况
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Println(m.HeapInuse)

test := make(map[int]int)
for i := 0; i < 10000; i++ {
test[i] = i
}

runtime.GC()
runtime.ReadMemStats(&m)
fmt.Println(m.HeapInuse)

for k := range test {
delete(test, k)
}

runtime.GC()
runtime.ReadMemStats(&m)
fmt.Println(m.HeapInuse)

runtime.KeepAlive(test)

defer

defer 语句指定在函数退出前执行的内容

func doClientWork(clientChan <-chan *rpc.Client) {
client := <-clientChan
defer client.Close()

var reply string
err = client.Call("HelloService.Hello", "hello", &reply)
if err != nil {
log.Fatal(err)
}

fmt.Println(reply)
}

从管道去取一个 RPC 客户端对象,并且通过 defer 语句指定在函数退出前关闭客户端。然后是执行正常的 RPC 调用 HelloService.Hello ,方法调用写法如下

func main() {
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}

clientChan := make(chan *rpc.Client)

go func() {
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}

clientChan <- rpc.NewClient(conn)
}
}()

doClientWork(clientChan)
}

make 和 new

  • make 的作用是初始化内置的数据结构,也就是切片哈希表Channel 等;
  • new 的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针;
i := new(int)
// 等价于
var v int
i := &v

switch

func main() {
var grade string = "B"
var marks int = 90
switch marks {
case 90:
grade = "A"
case 80:
grade = "B"
case 50, 60, 70:
grade = "C"
default:
grade = "D"
}

switch {
case grade == "A":
fmt.Printf("优秀!\n")
case grade == "B", grade == "C":
fmt.Printf("良好\n")
case grade == "D":
fmt.Printf("及格\n")
case grade == "F":
fmt.Printf("不及格\n")
default:
fmt.Printf("差\n")
}

var x interface{}

switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T", i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型")
default:
fmt.Printf("未知型")
}
}

结果:

优秀!
你的等级是 A
x 的类型 :<nil>

.(type)

只能在 switch case 中使用,如

type FooInterface interface{}
var x FooInterface
switch x.(type) {
case int:
fmt.Println(x, "is an int value.")
case string:
fmt.Println(x, "is a string value.")
case int64:
fmt.Println(x, "is an int64 value.")
default:
fmt.Println(x, "is an unknown type.")
}

结果

<nil> is an unknown type.

select

用于监测各个 Channel 的数据流动

  • 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行
  • case 后面必须是 channel 操作,否则报错
  • select 中的 default 子句总是可运行的。所以没有 default 的 select 才会阻塞等待事件
  • 没有运行的 case,那么将会阻塞发生报错(死锁)

示例

监视三个信道的数据流出并收集数据到一个信道中

func foo(i int) chan int {
c := make(chan int)
go func () { c <- i }()
return c
}


func main() {
c1, c2, c3 := foo(1), foo(2), foo(3)

c := make(chan int)

go func() { // 开一个goroutine监视各个信道数据输出并收集数据到信道c
for {
select { // 监视c1, c2, c3的流出,并全部流入信道c
case v1 := <- c1: c <- v1
case v2 := <- c2: c <- v2
case v3 := <- c3: c <- v3
}
}
}()

// 阻塞主线,取出信道c的数据
for i := 0; i < 3; i++ {
fmt.Println(<-c) // 从打印来看我们的数据输出并不是严格的1,2,3顺序
}
}

结果

3
2
1

同时开启三个 Goroutine 并发执行,直到发送信号后阻塞 Goroutine,等待每个 Goroutine 逐个取走信号,然后一个一个全部发送到 c(这里是主线程收一个才会发下一个)
注意:死循环的 for 需放在 Goroutine 中,否则之后的代码不会被执行

示例判断阻塞

判断 channel 是否阻塞(或者说 channel 是否已经满了)

package main

import (
"fmt"
)

func main() {
ch := make(chan int, 1) // 注意这里给的容量是1
ch <- 1
select {
case ch <- 2:
default:
fmt.Println("通道channel已经满啦,塞不下东西了!")
}
}

结果

通道channel已经满啦,塞不下东西了!

示例超时

超时机制

package main

import (
"fmt"
"time"
)

func main() {
ch := make(chan int)
select {
case <-ch:
case <-time.After(time.Second * 1): // 利用time来实现,After代表多少时间后执行输出东西
fmt.Println("超时啦!")
}
}

结果

超时啦!

通过 for range 读取信道中的多个消息

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3

// 显式地关闭信道
close(ch)

for v := range ch {
fmt.Println(v)
}

见:range 循环读取 channel 信息

for

func main() {
sum := 0
for i := 0; i <= 10; i++ {
sum += i
}
fmt.Println(sum)

sum2 := 1
for sum2 <= 10 {
sum2 += sum2
}
fmt.Println(sum2)

// 类似 While 语句形式
for sum2 <= 10 {
sum2 += sum2
}
fmt.Println(sum2)

sum3 := 0
for {
sum3++ // 死循环
}
fmt.Println(sum3) // 无法输出
}

结果:

55
16
16

注意如果 select 外层还有 for 循环,则 select 内部的循环只能 break select 本身,并不能 break for

反转

func Reverse(s []byte) {
for i, j := 0, len(s)-1; i < len(s)/2; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}


循环时内存地址的变化

func main() {
tasks := []func(){
func() { fmt.Printf("1. \n") },
func() { fmt.Printf("2. \n") },
func() { fmt.Printf("3. \n") },
func() { fmt.Printf("4. \n") },
}

for key, task := range tasks {
fmt.Printf("task: %p %p %v\n", &task, &key, key)
i := key
fmt.Printf("i: %p %v\n", &i, i)
}
}

结果

task: 0xc000006028 0xc000014098 0
i: 0xc0000140b8 0
task: 0xc000006028 0xc000014098 1
i: 0xc0000140d0 1
task: 0xc000006028 0xc000014098 2
i: 0xc0000140d8 2
task: 0xc000006028 0xc000014098 3
i: 0xc0000140e0 3
  • 循环时 key value 内存地址不变,根据每次循环时key task内存地址无变化,每次循环覆盖内存中的值,所以一定要注意循环中取key value的地址的情况(取到的内存地址都是相同的),实际内存地址中存储的值为最后一个
  • 循环时新定义的变量 i 每次都申请新的内存,但内存并不增加,应该是实时回收了,见下方测试
func main() {
tasks := []func(){
func() { fmt.Printf("1. \n") },
func() { fmt.Printf("2. \n") },
func() { fmt.Printf("3. \n") },
func() { fmt.Printf("4. \n") },
}

var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Println(m.HeapInuse)

for key, task := range tasks {
fmt.Printf("task: %p %p %v\n", &task, &key, key)
i := key * 100123231230000
fmt.Printf("i: %p %v\n", &i, i)

runtime.ReadMemStats(&m)
fmt.Println(m.HeapInuse)
}
}

结果

409600
task: 0xc000006030 0xc0000140d0 0
i: 0xc0000140d8 0
417792
task: 0xc000006030 0xc0000140d0 1
i: 0xc0000140e8 100123231230000
417792
task: 0xc000006030 0xc0000140d0 2
i: 0xc000014108 200246462460000
417792
task: 0xc000006030 0xc0000140d0 3
i: 0xc000014128 300369693690000
417792
  • 虽然每次 i 的内存地址不同,但内存并没有增加
  • 即使提前申明变量 i,使内存地址相同,内存占用是相同的,见下方测试
func main() {
tasks := []func(){
func() { fmt.Printf("1. \n") },
func() { fmt.Printf("2. \n") },
func() { fmt.Printf("3. \n") },
func() { fmt.Printf("4. \n") },
}

var i int
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Println(m.HeapInuse)

for key, task := range tasks {
fmt.Printf("task: %p %p %v\n", &task, &key, key)
i = key * 100123231230000
fmt.Printf("i: %p %v\n", &i, i)

runtime.ReadMemStats(&m)
fmt.Println(m.HeapInuse)
}
}

结果

409600
task: 0xc000006030 0xc0000140d0 0
i: 0xc000014098 0
417792
task: 0xc000006030 0xc0000140d0 1
i: 0xc000014098 100123231230000
417792
task: 0xc000006030 0xc0000140d0 2
i: 0xc000014098 200246462460000
417792
task: 0xc000006030 0xc0000140d0 3
i: 0xc000014098 300369693690000
417792

range 循环读取 channel 信息

func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3

fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
}

修改为

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3

// 显式地关闭信道
close(ch)

for v := range ch {
fmt.Println(v)
}

注意:range 不等到信道关闭是不会结束读取的。也就是如果缓冲信道干涸了,那么 range 就会阻塞当前 goroutine,这时需要显式的关闭信道,防止死锁

break

  • 跳出循环
  • switch执行case后跳出switch
  • 多重循环跳出指定 label

调度器

默认地, Go 所有的 goroutines 只能在一个线程里跑
如果当前 goroutine 不发生阻塞,它是不会让出 CPU 给其他 goroutine 的

内存

  • 编译期决定内存分配在何处
  • 每个 Goroutine 都有独立的栈空间

查看当前内存占用

var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Println(m.HeapInuse)

m.HeapInuse单位 bytes

GC

  • gc 管理堆(Heap)内存
  • c 语言编写的动态库 cgo 程序,不受 gc 管控
  • // go:notinheap不受 gc 管控
  • STW(Stop-The-World)
    • 暂停住所有正在执行的 Goroutine,通过暂停所有 P(可被使用的资源)来实现暂停 Goroutine
    • 1.16 使用抢占机制实现暂停资源
      • 方法入口编译时增加栈顶指针是否超过阈值的判断(if g.stackguard0 <= sp),
      • 如果超过,则执行某方法,通过方法内修改 P 的状态,G 的状态达到暂停的目的
      • 当 Goroutine 执行到方法入口时则暂停成功
      • 触发方式:通过在 Goroutine 外部修改 G 的状态(g.stackguard0)使满足条件,当执行到方法入口时就停下来了

race 竞争检测 并发检测

go run -race main.go
go build -race main.go

示例:a.go

package main

import(
"time"
"fmt"
)

func main() {
a := 1
go func(){
a = 2
}()
a = 3
fmt.Println("a is ", a)

time.Sleep(2 * time.Second)
}
> go run -race a.go
a is 3
==================
WARNING: DATA RACE
Write at 0x00c00009e058 by goroutine 7:
main.main.func1()
D:/htdocs/test/a.go:11 +0x44

Previous write at 0x00c00009e058 by main goroutine:
main.main()
D:/htdocs/test/a.go:13 +0x92

Goroutine 7 (running) created at:
main.main()
D:/htdocs/test/a.go:10 +0x84
==================
Found 1 data race(s)
exit status 66

goroutine 7 运行到第 11 行和 main goroutine 运行到 13 行的时候触发竞争,而且 goroutine 7 是在第 12 行的时候创建的

GOPROXY

GOPROXY https://goproxy.io,direct

资料查找

  • map safe site:golang.org
请我喝杯咖啡吧 Coffee time !