go 基础知识

495 阅读8分钟

应用入口

  1. 必须是 main 包:package main
  2. 必须是 main 方法:func main()
  3. 文件名不一定是 main.go

应用入口返回值

运行 go run main.go 会输出 exit status 255

func main() {
  os.Exit(-1)
}

不能使用 return 的形式

// 错误
func main() {
  return -1
}

获取命令行参数

通过 os.Args 获取命令行参数

func main() {
  fmt.Println(os.Args)
}

main 函数不支持传入参数

// 错误
func main(arg []string){}

变量声明

  1. var a int = 1
  2. var b, c int = 1, 2
  3. 可以换行
    var (
      d int = 1
      e int = 2
    )
    
  4. a := 1,只能在函数内部使用

常量

const (
  C0 = iota
  C1
  C2
)
fmt.Println(C0, C1, C2) // 0 1 2

iota 第一次出现的时候值是 0

语法糖:如果每句话都差不多,可以省略

const (
  C0 = iota
  C1 = iota
  C2 = iota
)
// 等价于
const (
  C0 = iota
  C1
  C2
)

iota

  1. iota 默认是 0,每行加 1
  2. 只能在 const() 中使用

字符串

  1. string 是值类型,默认的初始值是空字符串,不是 nil
  2. string 是只读的 byte slice,len 函数返回的是它所包含的 byte 数,这个 byte 和字符是不一样的
  3. string 的 byte 数组可以存放任何数据
s := "\xE4\xB8\xA5"
fmt.Println(s) // 严
fmt.Println(len(s)) // 3

Unicode 和 UTF-8

字符“中”
Unicode0x4E2D
UTF-80xE4B8AD
string/[]byte[0xE4, 0xB8, 0xAD]

rune 可以获取字符的 Unicode

s := "中"
fmt.Println(len(s)) // 3
c := []rune(s)
fmt.Println(len(c)) // 1
fmt.Printf("中 Unicode:%x", c) // 4e2d
fmt.Printf("中 UTF-8:%x", s) // e4b8ad

高效拼接字符串 —— strings.Builder

var builder strings.Builder
for i := 0; i < 10; i++ {
  builder.WriteString(strconv.Itoa(i))
}
str := builder.String()

高效拼接字符串 —— bytes.Buffer

var buf bytes.Buffer
for i := 0; i < 10; i++ {
  buf.WriteString(strconv.Itoa(i))
}
str := buf.String()

数据类型

  1. bool
  2. string
  3. int、int8、int16、int32、int64
  4. uint、uint8、uint16、uint32、uint64、uinttptr
  5. byte:alias for uint8
  6. rune:alias for int32, represents a Unicode code point
  7. float32、float64
  8. complex64、complex128

类型转换

  1. 不允许隐式类型转换
  2. 别名和原有类型也不能进行隐式类型转换
type MyInt int64
func TestImplicit(t *testing.T){
  var a int32 = 1
  var b int64
  var c MyInt
  b = a // 报错
  b = int64(a)
  c = b // 报错
  c = MyInt(b)
}

位运算

&^ 按位清零

  1. 1 &^ 0 -> 1
  2. 1 &^ 1 -> 0
  3. 0 &^ 1 -> 0
  4. 0 &^ 0 -> 0

如果后面一位是 1 输出为 1;如果后面一位是 0 前面一位是啥,输入就是啥

const (
  Readable = 1 << iota
  Writeable
  Executable
)

func TestBitClear(t *testing.T){
  a := 7
  fmt.Println(a & Readable == Readable) // true
  a = a &^ Readable
  fmt.Println(a & Readable == Readable) // false
}

for 循环

第一种:

i := 1
for i <= 3 {
  fmt.Println(i)
  i++
}

第二种:

for j := 1; j <= 3; j++ {
  fmt.Println(j)
}

第三种:

k := 1
for {
  if k > 3 {
    break
  }
  fmt.Println(k)
  k++
}

第四种:数组/map遍历

for _, e : range arr {
  fmt.Println(e)
}

if/switch

if

num := 9 只能在 if 语句中使用

if num := 9; num < 0 {
  fmt.Println(num, "is negative")
} else if num < 10 {
  fmt.Println(num, "has 1 digit")
} else {
  fmt.Println(num, "has multiple digits")
}

switch

  1. 不要加 break
  2. 一个 case 可以多个值,用逗号隔开
  3. 如果要贯穿,可以使用 fallthrough
switch time.Now().Weekday() {
  case time.Saturday, time.Sunday:
    fmt.Println("It's the weekend")
  default:
    fmt.Println("It's a weekday")
}

函数

方式一:

func f1(x, y int)(int, int){
  return x + y, x * y
}

方式二:

使用这种方式,可以直接返回值,如果在 return 后面再指定其他返回值,会覆盖掉 sum 和 product

func f2(x, y int) (sum, product int) {
  sum = x + y
  product = x * y
  return
}

使用:

a, b := f1(1, 2)
c, d := f2(2, 3)

函数是一等公民

  1. 可以有多个返回值
  2. 所有参数都是值传递:slice,map,channel 会有传引用的错觉
  3. 函数可以作为变量的值
  4. 函数可以作为参数和返回值

结构体

定义

type User struct {
  Username string
  Age      int
}

实例创建

  1. u1 := User{ UserName: "uccs" , Age: 16 }
  2. u2 := User{ "uccs", 16 }
  3. u3 := new(User)
    • u3.UserName = "uccs"
    • u3.Age = 16

第三种方式返回的是指针,相当于 &User{}

fmt.Println("%T", u3) // *encap.User
fmt.Println("%T", u1) // encap.User

把结构体当成参数

// 传递的是值,不是地址
func modify(u User){
  u.Age = 12
}

func main(){
  u := User{ "uccs", 2 }
  modify(u)
  fmt.Println(u) // {"uccs", 2}
}

传递地址

用于类型时:用 *,用于值时:用 &(*u 表示 u 对应的值)

// 传递的是地址,不是值
func modify(u *User){
  // *u 表示取地址的值
  // (*u).Age = 22
  // 下面的是语法糖,等价于上面的
  u.Age = 22
}
func main(){
  u := User{ "uccs", 2 }
  modify(&u)
  fmt.Println(u) // {"uccs", 22}
}

传值和传指针的差异

在实例方法被调用时,实例的成员会进行复制

func modify(u User){
  fmt.Printf("%x\n", &u.Age) // c0000100b8
  u.Age = 12
}

func main(){
  u := User{ "uccs", 2 }
  modify(u)
  fmt.Printf("%x ", &u.Age) // c0000100a0
}

在实例方法被调用时,可以避免内存拷贝

func modify(u *User){
  fmt.Printf("%x\n", &u.Age) // c0000100a0
  u.Age = 22
}
func main(){
  u := User{ "uccs", 2 }
  modify(&u)
  fmt.Printf("%x\n", &u.Age) // c0000100a0
}

支持 label

type User struct {
  Username string `json:"username"`
  Age      int    `json:"age"`
}

func main() {
  u := User{Username: "astak", Age: 16}
  bytes, error := json.Marshal(u)
  if error != nil {
    fmt.Println(error)
  }
  fmt.Println(string(bytes)) // {"username":"astak","age":16}
}

数组

定长

a := [3]int{1, 2, 3}
// 可以省略长度
a := [...]int{1, 2, 3}

数组的比较

长度相同的数组可以进行比较

a := [...]int{1, 2, 3}
b := [...]int{1, 2, 3}
c := [...]int{1, 2}

fmt.Println(a == b) // true
fmt.Println(a == c) // 报错

数组截取

a := [...]int{1, 2, 3, 4, 5}
a[1:2] // 2
a[1:3] // 2, 3
a[1:len(a)] // 2, 3, 4, 5
a[1:] // 2, 3, 4, 5
a[:3] // 1, 2, 3

切片

字面量声明

array := [...]int{1, 2, 3}
slice := []int{1, 2, 3}

taSlice := reflect.TypeOf(slice)
taArray := reflect.TypeOf(array)

fmt.Println(taSlice.Kind()) // slice
fmt.Println(taArray.Kind()) // array

开辟一块内存

语法:make([]type, len, cap)

s := make([]int, 3)

遍历

s := []int{1, 2, 3, 4}
for i, v := range s {
  fmt.Println(i, v)
}

追加

s := []int{1, 2, 3, 4}
s = append(s, 5)

slice 的特点

type slice struct {
  array unsafe.Pointer
  len int
  cap int
}
  • array 指向底层数组的指针
  • len 长度
  • cap 容量

追加内容时,如果 cap 不够,就复制到新的更长的数组,扩容时 slice 对应的结构体会被复用

指针

将 * 放在类型前面,表示声明一个指针类型,将 * 放在变量前面,表示取这个变量的值

slice 和 map,go 没有为它们提供自动的指针操作,因为他们本身就是引用类型

i := 1

  • iPtr := &i,iPtr 的值是 i 的地址
  • iPtr := *&i,iPtr 的值是 i 的内容
func zeroValue(value int) int {
  value = 0
  return value
}

func zeroPointer(ptr *int) {
  (*ptr) = 0
}

func main() {
  i := 1
  iPtr := &i
  zeroValue(i)
  fmt.Println(i)
  zeroPointer(iPtr)
  fmt.Println(*iPtr)
}

用指针获取 j 的值

i := 1
j := 2
iPtr := &i
println(i, j, &i, &j)
println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(iPtr)) - unsafe.Sizeof(i))))

用指针遍历 slice

vals := []int{10, 20, 30, 40}
start := unsafe.Pointer(&vals[0])
// 每一个元素的长度,int 长度都一样
size := unsafe.Sizeof(int(0))

for i := 0; i < len(vals); i++ {
  item := *(*int)(unsafe.Pointer(uintptr(start) + size*uintptr(i)))
  fmt.Println(item)
}

将指针作为函数的参数

go 的函数和方法都是按值传递参数的,所以函数总是操作参数的副本

当指针作为参数传递时,函数将接收传入的内存地址的副本,之后函数可以通过解引内存地址来修改指针指向的值

如果一种类型的某些方法需要用到指针作为接收者,那么这种类型的所有方法都应该用指针作为接收者

结构体作为参数

growUp 方法要接收指针类型的参数,否则没法操作 person 的属性

type person struct {
  name string
  age  int
}

func (p *person) growUp() {
  p.age++
}

func main() {
  terry := person{
    name: "terry",
    age:  18,
  }
  terry.growUp()
  fmt.Println(terry)

  nathan := &person{
    name: "nathan",
    age:  18,
  }
  nathan.growUp()
  fmt.Println(nathan)
}

切片作为参数

切片作为参数时,传指针和传值的区别,都会修改原切片的值(map 也是一样)

type MySlice []int

func (s *MySlice) modifySlice() {
  (*s)[1] = 100
}
func (s MySlice) modifySlice2() {
  s[2] = 200
}
func main() {
  s := MySlice{1, 2, 3, 4, 5}
  s.modifySlice()
  fmt.Println(s)

  s.modifySlice2()
  fmt.Println(s)
}

获取结构体中字段的指针

& 操作符可以获得结构体的内存地址,还可以获得结构体中指定字段的内存地址

type stats struct {
  level             int
  endurance, health int
}
func levelUp(s *stats) {
  s.level++
  s.endurance = 42
  s.health = 5
}
type character struct {
  name  string
  stats stats
}
func main(){
  player := character{name: "Matthias"}
  levelUp(&player.stats)
  fmt.Println(player.stats)
}

map

语法:

  1. s1 := make(map[string]int, cap)
  2. s1 := map[string]int{ "apple": 2, "orange": 2 }
  3. s1 := map[string]int{}
s1 := make(map[string]int, 10)
fmt.Println(s1)
myMap := make(map[string]int)

var myMap map[string]int

检测 key 是否存在

map 在访问的 key 不存在时,仍会返回零值,不能通过返回 nil 来判断元素是否存在

if v, hasKey := myMap["apple"]; hasKey {
  fmt.Println("key apple's is %s", v)
}

关联

将 celsius 和 kelvin 关联起来了

预声明的类型是不能进行关联的,比如 int、float64 等

type kelvin float64
type celsius float64

func kelvinToCelsius(k kelvin) celsius {
  return celsius(k - 273.15)
}

func (k kelvin) celsius() celsius {
  return celsius(k - 273.15)
}

func main() {
  var k kelvin = 294.0
  var c celsius
  c = kelvinToCelsius(k)
  c = k.celsius()
  fmt.Println(c)
}

序列化

type Person struct {
  Name string
  age  int
}
p1 := Person{"uccs", 16}

xml 序列化和反序列化

序列化

xml.Marshal 输出的文本是没有格式的

xml.MarshalIndent 输出的文本是有格式的,xml.MarshalIndent(p1, prefix, suffix)

p1 := Person{"uccs", 16}
var data []byte
var err error
if data, err = xml.Marshal(p1); err != nil {
  fmt.Println(err)
  return
}
fmt.Println(string(data))

反序列化

p2 := new(Person)
if err = xml.Unmarshal(data, p2); err != nil {
  fmt.Println(err)
  return
}
fmt.Println(p2)

变成属性

type Person struct {
  Name string `xml:"name,attr"`
  Age  int
}

json 序列化和反序列化

序列化

p1 := Person{"uccs", 16}
var data []byte
var err error
if data, err = json.Marshal(p1); err != nil {
  fmt.Println(err)
  return
}
fmt.Println(string(data))

反序列化

p2 := new(Person)
if err = json.Unmarshal(data, p2); err != nil {
  fmt.Println(err)
  return
}
fmt.Println(p2)

获取命令行参数

fmt.Println(os.Args)

自定义参数

自定义参数 -method 和 -value

methodStr := flag.String("method", "default", "method of sample")
valuePtr := flag.Int("value", -1, "value of sample")

flag.Parse()

fmt.Println(*methodStr, *valuePtr)

自定义参数内容

var method string
var value int
flag.StringVar(&method, "method", "default", "method of sample")
flag.IntVar(&value, "value", -1, "value of sample")

flag.Parse()

fmt.Println(method, value)

测试

  1. 源码文件以 _test 结尾:xxx_test.go
  2. 测试方法名以 Test 开头:func TestXXX(t *testing.T){...}

benchmark

在命令行中执行 go test -bench=.,更详细的信息可以使用 go test -bench=. -benchmem

func Benchmark(b testing.B){
  // 与性能测试无关的代码
  b.ResetTimer()
  for i := 0; i < b.N; i++ {
    // 测试代码
  }
  b.StopTimer()
  // 与性能测试无关的代码
}

错误处理

  1. error 类型实现了 error 接口
    type error interface {
      Error() string
    }
    
  2. 通过 errors.New 快速创建错误实例
    errors.New("n must be in the range [0, 10]")
    
  3. 类似 java 中的 try...catch(err)...
    func main(){
      defer func() {
        if err := recover(); err != nil {
          fmt.Println("recovered from", err)
        }
      }()
      panic(errors.New("Something wrong!"))
    }
    

退出

  1. panic
    • 用于不可恢复的错误
    • 退出前会执行 defer 指定的内容
  2. os.Exit
    • 退出时不会调用 defer 执行的函数
    • 退出时不输出当前调用栈信息

package

  1. 以首字母大写来表明可被包外代码访问
  2. 代码的 package 可以和所在目录不一致
  3. 同一目录里的 Go 代码的 package 要保持一致

init

  1. 在 main 被执行前,所有依赖的 package 的 init 方法都会被执行
  2. 不同包的 init 函数按照包导入的依赖关系决定执行顺序
  3. 每个包可以有多个 init 函数
  4. 包的每个源文件也可以有多个 init 函数,这点比较特殊

下载

  1. 通过 go get 获取远程依赖
    • go get -u 强制从网络更新远程依赖
  2. 注意代码在 GitHub 上的组织形式,以适应 go get
    • 直接以代码路径开始,不要有 src

Context

  • 根 Content:通过 context.Background() 创建
  • 子 Context:通过 context.WithCancel(parentContext) 创建
    • ctx, cancel := context.WithCancel(context.Background())
  • 当前 Context 被取消时,基于它的子 Context 都会被取消
  • 接收取消通知 <-ctx.Done()
func isCancelled(ctx context.Context) bool {
  select {
  case <-ctx.Done():
    return true
  default:
    return false
  }
}
func TestCloseChannel(t *testing.T) {
  ctx, cancel := context.WithCancel(context.Background())
  for i := 0; i < 5; i++ {
    go func(i int, ctx context.Context) {
      for {
        if isCancelled(ctx) {
          break
        }
        time.Sleep(time.Millisecond * 5)
      }
      fmt.Println(i, "cancelled")
    }(i, ctx)
  }
  cancel()
  time.Sleep(time.Second * 1)
}

输出

  1. fmt.Printf("%x", 17) 输出 16 进制
  2. fmt.Printf("%b", 17) 输出 2 进制
  3. fmt.Printf("%T", 17) 输出类型
  4. fmt.Printf("%c", 'a') 输出字符
  5. fmt.Printf("%q", "\"golang\"") 输出带引号的字符串:"\"golang\""
  6. fmt.Printf("%v", location{lat: 4, lon: 5}),输出没有 key
  7. fmt.Printf("%+v", location{lat: 4, lon: 5}),输出有 key
  8. fmt.Printf("%#v", location{lat: 4, lon: 5}),输出 key 和 value,并且带有类型
  9. fmt.Printf("Name: value(%[1]v), Type(%[1]T)\n", "uccs"),表示都是用第一个值

组合和转发

  1. report 是由 temperature 和 location 组合得到的
  2. average 方法接收的参数是 temperature,调用 average 方法就有两种方式:
    • t.average()
    • report.temperature.average()
type report struct {
  sol         int
  temperature temperature
  location    location
}
type temperature struct {
  high, low celsius
}
type location struct {
  lat, long float64
}
type celsius float64
func (t temperature) average() celsius {
  return (t.high + t.low) / 2
}
func main(){
  bradbury := location{-4.5895, 137.4417}
  t := temperature{high: -1.0, low: -78.0}
  report := report{sol: 15, temperature: t, location: bradbury}

  fmt.Printf("%+v", report)
  fmt.Printf("%v", t)
}

转发

r.report.average() 调用的是 temperature 的 average 方法,这就实现了转发

func (r report) average() celsius {
  return r.temperature.average()
}

fmt.Printf("%v", report.average())

嵌入

嵌入是之使用类型,不使用名称,这样就可以直接调用 temperature 的 average 方法

使用嵌入,go 会自动将 temperature 和 location 的方法都加到 report 中

调用 average 方法就有两种方式:

  • report.temperature.average()
  • report.average()
type report struct {
  sol int
  temperature
  location
}
func (t temperature) average() celsius {
  return (t.high + t.low) / 2
}
fmt.Printf("%v", report.temperature.average())
fmt.Printf("%v", report.average())

转发冲突

如果 report 中有 temperature 和 location 都有 average 方法,就会出现转发冲突

使用 report.average 就会报错,不使用就不会报错

func (t temperature) average() celsius {
  return (t.high + t.low) / 2
}

func (l location) average() celsius {
  return 1
}

fmt.Printf("%v", report.average()) // 使用报错,不使用不会报错

如果想要调用使用 report.average(),就需要再 report 上定义一个 average 方法

func (r report) average() celsius {
  return r.temperature.average()
}

接口

任何类型的任何值,只要满足了接口的方法,就是实现了接口

声明变量 t,它的类型是一个接口,这个接口有一个方法 talk,返回值是 string

martian 实现了 talk 方法,所以 t 就有 talk方法

var t interface {
  talk() string
}
type martian struct{}
func (m martian) talk() string {
  return "nack nack"
}
func main() {
  t := martian{}
  fmt.Println(t.talk())
}

声明类型 talker,它的类型是一个接口,这个接口有一个方法 talk,返回值是 string

函数 shout 接收 talker 类型的参数

martian 实现了 talk 方法,所以 t 就有 talk方法

type talker interface {
  talk() string
}
type martian struct {}
func (m martian) talk() string {
  return "nack nack"
}
func shout(t talker) {
  louder := strings.ToUpper(t.talk())
  fmt.Println(louder)
}
func main() {
  t := martian{}
  shout(t)
  // 或者这样使用
  // fmt.Println(t.talk())
}

fmt.Println() 就是利用这个特性

只要实现了 String() 方法,就可以使用 fmt.Println() 输出

type User struct{ Name string }
func (u User) String() string {
  return "我叫 xxx"
}
func main() {
  u := User{"uccs"}
  fmt.Println(u) // 我叫 xxx
}

nil

nil 会导致 panic

如果指针没有明确的指向,那么程序将无法对其实施解引用;如果尝试解引用一个 nil 指针将导致程序崩溃

var nowhere *int
fmt.Println(nowhere) // nil
// 对值为 nil 的指针进行解引用操作,会引起 panic
fmt.Println(*nowhere)

保护方法

值为 nil 的接收者和之为 nil 的参数在行为上并没有区别,所以在接收者为 nil 的情况下,也会继续调用方法

type person struct{
  age int
}
func (p *person) birthday(){
  // 这里会报错
  p.age++
}
func main(){
  var nobody *person
  fmt.Println(nobody)
  // 不会报错,还是会调用 birthday 方法
  nobody.birthday()
}

nil 函数值

当变量被声明为函数类型时,它的默认值是 nil

var fn func(a, b int) int

fmt.Println(fn == nil) // true

检查函数值是否为 nil,并在需要时提供默认行为

func sortStrings(s []string, less func(i, j int) bool) {
  if less == nil {
    les = func(i, j int) bool { return s[i] - s[j] }
  }
  sort.slice(s, less)
}
func main() {
  food := []string{"onion", "carrot", "celery"}
  sortStrings(food, nil)
  fmt.Println(food) // ["carrot", "celery", "onion"]
}

nil slice

如果 slice 在声明之后没有使用复合字面值或内置的 make 函数进行初始化,那么它的值为 nil

range、len、append 等都可以正常处理值为 nil 的 slice

var soup []string
fmt.Println(soup == nil) // true

// 这段代码不会走,因为 slice 的值为 nil,所以不会执行 for 循环
for _, ingredient := range soup {
  fmt.Println(ingredient)
}

fmt.Println(len(soup)) //

soup = append(soup, "onion")
fmt.Println(soup) // ["onion"]

空的 slice 和值为 nil 的 slice 不相等,但它们可以替换使用

nil map

和 slice 一样,如果 map 在声明后没有使用复合字面值或内置的 make 函数进行初始化,那么它的值为 nil

var soup map[string]int
fmt.Println(soup == nil) // true

// 不会报错,ok 为 false
measurement, ok := soup["onion"]
if ok {
  fmt.Println(measurement)
}

// 这段代码不会走,因为 map 的值为 nil,所以不会执行 for 循环
for ingredient, amount := range soup {
  fmt.Println(ingredient, amount)
}

nil 接口

  1. 声明为接口类型的变量在未被初始化时,其值为 nil
  2. 对于一个未被赋值的接口变量来说,它的接口类型和值都是 nil,并且变量本身也等于 nil
var v interface{}
// 类型为 nil
// 值为 nil
// true
fmt.Println("%T %v %v\n", v, v, v == nil)
  1. 当接口类型的变量被赋值后,接口就会在内部执行该变量的类型和值
  2. 在 go 中,接口类型的变量只有在类型和值都为 nil 时才等于 nil
    • 即使接口变量的值仍为 nil,但只要它的类型不为 nil,那么它就不等于 nil
var v interface{}
// v 类型为 nil
// v 值为 nil
// true
fmt.Printf("%T %v %v\n", v, v, v == nil)

var p *int
v = p
// v 类型为 *int
// v 值为 nil
// false,虽然值为 nil,但类型不为 nil,所以这里为 false
fmt.Printf("%T %v %v\n", v, v, v == nil)
  1. 接口变量内部表示
fmt.Printf("%#v\n", v)

nil 另一种用法

type number struct{
  value int
  valid bool
}
func newNumber(v int) number{
  return number{value: v, valid: true}
}
func (n number) String() string{
  if !n.valid {
    return "not set"
  }
  return fmt.Sprint("%d", n.value)
}
func main(){
  n := newNumber(42)
  fmt.Println(n) // 42
  e := number{}
  fmt.Println(e) // not set
}

goroutine

在 go 中,独立的任务叫做 goroutine

  • 虽然 goroutine 与其他语言中的协程、进程、线程等概念有些相似,但它们之间并不是完全相同
  • goroutine 创建效率是非常高的
  • go 能直截了当的协同多个并发 concurrent 操作

在 go 中,无需修改现有顺序式的代码,就可以通过 goroutine 以并发的方式运行任意数量的任务

在 main 函数返回时,该程序运行的所有 goroutine 都会立即停止(不论有没有运行完)

需要注意的是即使已经停止等待 goroutine,但只要 main 函数还没有返回,仍在运行的 goroutine 将会继续占用内容

func sleepyGopher() {
  time.Sleep(3 * time.Second)
  fmt.Println("... snore ...")
}
func main() {
  go sleepyGopher() // 分支线路
  time.Sleep(4*time.Second) // 主干路
}

不止一个 goroutine

每次使用 go 关键字都会产生一个新的goroutine

表面上看,goroutine 似乎在同时运行,但由于计算机处理单元有限,其实技术上来说,这些goroutine 不是真的在同时运行

  • 计算机处理器会使用分时技术,在多个 goroutine 上轮流花费一些时间
  • 在使用 goroutine 时,各个 goroutine 的执行顺序无法确定
func sleepyGopher(i int) {
  time.Sleep(3 * time.Second)
  fmt.Println("... snore ...", i) // 这里输出的 i 不一定是按照顺序输出的
}
func main() {
  for i := 0; i < 5; i++ {
    go sleepyGopher(i) // 分支线路
  }
  time.Sleep(4*time.Second) // 主干路
}

channel

  • 通过 channel 可以在多个 goroutine 之间安全的传值
  • 通道可以用作变量、函数参数、结构体字段
  • 创建通道用 make 函数,并指定其传输数据的类型:c := make(chan int)
  • 接收/发送
    • 发送操作会等待直到另一个 goroutine 尝试对该通道进行接收操作为止
      • 执行发送操作的 goroutine 在等待期间将无法执行其他操作
      • 未在等待通道操作的 goroutine 仍然可以继续自由的运行
  • 执行接收操作的 goroutine 将等待直到另一个 goroutine 尝试向该通道进行发送操作为止
// 1. 创建 chan,需要指定类型
ch := make(chan string)
// 2. 启动 go 程
go func() {
  time.Sleep(1 * time.Second)
// 3. 往 chan 发送一个值
  ch <- "ping"
}()
// 4. 从 chan 接收一个值
msg := <-ch
fmt.Println(msg)

阻塞和死锁

  • 当 goroutine 在等待通道发送或接收时,这种情况称为阻塞
  • 除了 goroutine 本身占有少量的内存外,被阻塞的 goroutine 不会占用任何其他资源
    • goroutine 静静的停在那里,等待导致被阻塞的事情来解除阻塞
  • 当一个或多个 goroutine 因为某些永远无法发声的事情被阻塞时,这种情况就是死锁,出现死锁的程序通常会崩溃或者挂起
// 死锁,这段程序没有给 chan 传值
func main(){
  c := make(chan int)
  <- c
}
// 解锁
func main(){
  c := make(chan int)
  go func(){ c <- 1 }() // 给 chan 传值
  <- c
}

关闭通道

  • go 允许在没有值可供发送的情况下通过 close 函数关闭通道
  • 通道被关闭后无法写入任何值,如果尝试写入将引发 panic
  • 尝试读取被关闭的通道会获得与通道类型对应的零值

如果在循环里读取一个已关闭的通道,并没有检查通道是否关闭,那么该循环可能会一直运转下去,消耗系统资源

检查通道是否关闭:v, ok := <-ch,如果 ok 为 false,那么通道已经关闭

方法一

  1. sourceGopher 函数负责向 downstream 通道发送数据
    • 如果没有数据了,就向通道中发送空字符串
  2. filterGopher 函数负责过滤 upstream 通道中的数据
    • 用 item 接收 upstream 通道中的数据
    • 无限循环,如果 item 为 "" 跳出循环
    • 过滤掉通道中包含 "bad" 的字符串后赋值给 downstream
  3. printGopher 函数负责打印 upstream
    • 用 v 接收 upstream 通道中的数据
    • 无限循环,如果 item 为 "" 跳出循环
    • 打印 v
  4. main 函数负责调用 sourceGopher、filterGopher、printGopher 函数
func sourceGopher(downstream chan string) {
  for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
    downstream <- v
  }
  downstream <- ""
}
func filterGopher(upstream, downstream chan string) {
  for {
    item := <-upstream
    if item == "" {
      downstream <- ""
      return
    }
    if !strings.Contains(item, "bad") {
       downstream <- item
    }
  }
}
func printGopher(upstream chan string) {
  for {
    v := <-upstream
    if v == "" {
      return
    }
    fmt.Println(v)
  }
}
func main() {
  c0 := make(chan string)
  c1 := make(chan string)
  go sourceGopher(c0)
  go filterGopher(c0, c1)
  printGopher(c1)
}

方法二

方法二和方法一的区别是:go 提供了 close 关闭通道

  • 在接收通道的值时,通过判断第二个参数是否为 false 来判断通道是否关闭
func sourceGopher(downstream chan string) {
  for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
    downstream <- v
  }
  close(downstream)
}
func filterGopher(upstream, downstream chan string) {
  for {
    v, ok := <-upstream
    if !ok {
      close(downstream)
      return
    }
    if !strings.Contains(v, "bad") {
      downstream <- v
    }
  }
}
func printGopher(upstream chan string) {
  for {
    v, ok := <-upstream
    if !ok {
      return
    }
    fmt.Println(v)
  }
}
func main() {
  c0 := make(chan string)
  c1 := make(chan string)
  go sourceGopher(c0)
  go filterGopher(c0, c1)
  printGopher(c1)
}

方法三

从通道中取值,需要判断传值是否结束,所以 go 提供了 range 关键字,可以遍历通道,当通道关闭时,range 会自动退出循环

func sourceGopher(downstream chan string) {
  for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
    downstream <- v
  }
  close(downstream)
}
func filterGopher(upstream, downstream chan string) {
  for item := range upstream {
    if !strings.Contains(item, "bad") {
       downstream <- item
    }
  }
  close(downstream)
}
func printGopher(upstream chan string) {
  for v := range upstream {
    fmt.Println(v)
  }
}
func main() {
  c0 := make(chan string)
  c1 := make(chan string)
  go sourceGopher(c0)
  go filterGopher(c0, c1)
  printGopher(c1)
}

超时控制

当代码运行到 select 时,只要有一个 case 能够执行,就会执行;如果都是阻塞状态,有 default 就会执行 default,如果没有,就会阻塞

time.After 函数返回一个通道,该通道在指定事件后会接收到一个值(发送该值的 goroutine 是 go 运行时的一部分)

利用这种机制就可以实现超时控制:

AsyncService 如果超时了,就不再等待了,直接返回 time out

func TestSelect(t *testing.T) {
  select {
  case ret := <-AsyncService():
    t.Log(ret)
  case <-time.After(time.Millisecond * 100):
    t.Log("time out")
  }
}

select 语句在不包含任何 case 的情况下将永远等下去

nil 通道

  • 如果不使用 make 初始化通道,那么通道变量的值就是 nil(零值)
  • 对 nil 通道进行发送或接收不会引起 panic,但会导致永久阻塞
  • 对 nil 通道执行 close 函数,会引起 panic
  • nil 通道的用处:
    • 对于包含 select 语句的循环,如果不希望每次循环都等待 select 所涉及的所有通道,那么可以先将某些通道设为 nil,等待发送值准备就绪之后,再将通道变成一个非 nil 值并执行发送操作

并发控制

并发机制控制 —— 锁 [Lock]

如果不加锁,就出现了线程不安全操作,线程安全的情况下输出的是 5000,和我们预期是一样的

func TestCounterThreadSafe(t *testing.T) {
  var mut sync.Mutex
  counter := 0
  for i := 0; i < 5000; i++ {
    go func() {
      defer func() {   // 释放锁
        mut.Unlock()
      }()
      mut.Lock() // 加锁
      counter++
    }()
  }
  time.Sleep(1 * time.Second)
  t.Logf("counter = %d", counter) // "counter = 5000"
}

并发机制控制 —— WaitGroup

func TestCounterWaitGroup(t *testing.T) {
  var mut sync.Mutex
  var wg sync.WaitGroup
  counter := 0
  for i := 0; i < 5000; i++ {
    wg.Add(1) // 每启动一个协程加一次
    go func() {
      defer func() {
        mut.Unlock()
      }()
      mut.Lock()
      counter++
      wg.Done() // 有一个等待的已经完成了
    }()
  }
  wg.Wait()
  t.Logf("counter = %d", counter)
}

WaitGroup 比 time.Sleep 好的一点在于:WaitGroup 等待了每一个协程执行的执行,而 time.Sleep 只是预估了一个时间,实际多久并不知道

并发机制控制 —— CSP

串行

func service() string {
  time.Sleep(time.Millisecond * 50)
  return "Done"
}

func otherTask() {
  fmt.Println("working on something else")
  time.Sleep(time.Millisecond * 100)
  fmt.Println("Task in done.")
}
func TestService(t *testing.T) {
  fmt.Println(service())
  otherTask()
}

这里是按照顺序输出的(串行):

Done
working on something else
Task in done.

使用 chan

  1. service 调用时,启动另一个协程去运行,不阻塞当前的协程
  2. 返回结果时,返回 chan,外面需要结果的话,再去读取 chan

由于没有使用 buffer,所以这里会阻塞,直到 retCh 有值才会继续往下执行

也就是说这里阻塞了 service 的调用的协程

func service() string {
  time.Sleep(time.Millisecond * 50)
  return "Done"
}

func otherTask() {
  fmt.Println("working on something else")
  time.Sleep(time.Millisecond * 100)
  fmt.Println("Task in done.")
}

func AsyncService() chan string {
  retCh := make(chan string)
  go func() {
    ret := service()
    fmt.Println("returned result.")
    retCh <- ret  // 会阻塞
    fmt.Println("service exited.") // retCh 有值才会执行
  }()
  return retCh
}

func TestAsyncService(t *testing.T) {
  retCh := AsyncService()
  otherTask()
  fmt.Println(<-retCh)
  time.Sleep(time.Second * 1)
}

输出:

working on something else
returned result.
Task in done.
Done
service exited.

使用 chan buffer

释放 service 的调用的协程,让其继续往下执行

func service() string {
  time.Sleep(time.Millisecond * 50)
  return "Done"
}

func otherTask() {
  fmt.Println("working on something else")
  time.Sleep(time.Millisecond * 100)
  fmt.Println("Task in done.")
}
func TestService(t *testing.T) {
  fmt.Println(service())
  otherTask()
}

func AsyncService() chan string {
  retCh := make(chan string, 1)
  go func() {
    ret := service()
    fmt.Println("returned result.")
    retCh <- ret
    fmt.Println("service exited.")
  }()
  return retCh
}

func TestAsynService(t *testing.T) {
  retCh := AsyncService()
  otherTask()
  fmt.Println(<-retCh)
  time.Sleep(time.Second * 1)
}

输出:

working on something else
returned result.
service exited.
Task in done.
Done

单例

type Singleton struct{}

var singleInstance *Singleton
var once sync.Once

func GetSingletonObj() *Singleton {
  once.Do(func() {
    fmt.Println("Create Obj")
    singleInstance = new(Singleton)
  })
  return singleInstance
}

func TestGetSingletonObj(t *testing.T) {
  var wg sync.WaitGroup
  for i := 0; i < 10; i++ {
    wg.Add(1)

    go func() {
      obj := GetSingletonObj()
      fmt.Printf("%x\n", unsafe.Pointer(obj))
      wg.Done()
    }()
  }
  wg.Wait()
}

完成一个任务和完成所有任务

这里只接收了第一个协程的结果,只有第一个协程被释放了,通过 runtime.NumGoroutine() 可以看到

func runTask(i int) string {
  time.Sleep(10 * time.Millisecond)
  return fmt.Sprintf("The result is from %d", i)
}
func FirstResponse() string {
  numOfRunner := 10
  ch := make(chan string)
  for i := 0; i < numOfRunner; i++ {
    go func(i int) {
      ret := runTask(i)
      ch <- ret
    }(i)
  }
  return <-ch
}
func TestResponse(t *testing.T) {
  fmt.Println("Before: ", runtime.NumGoroutine())  // 2
  fmt.Println(FirstResponse()) // 只返回了第一个协程的结果
  time.Sleep(time.Second)
  fmt.Println("After: ", runtime.NumGoroutine()) // 11
}

接收了所有协程的结果,协程就会被释放,通过 runtime.NumGoroutine() 可以看到

func runTask(i int) string {
  time.Sleep(10 * time.Millisecond)
  return fmt.Sprintf("The result is from %d", i)
}
func AllResponse() string {
  numOfRunner := 10
  ch := make(chan string)
  for i := 0; i < numOfRunner; i++ {
    go func(i int) {
      ret := runTask(i)
      ch <- ret
    }(i)
  }
  finalRet := ""
  for j := 0; j < numOfRunner; j++ {
    finalRet += <-ch + "\n"
  }
  return finalRet
}
func TestResponse(t *testing.T) {
  fmt.Println("Before: ", runtime.NumGoroutine())  // 2
  fmt.Println(AllResponse()) // "......"
  time.Sleep(time.Second)
  fmt.Println("After: ", runtime.NumGoroutine()) // 2
}

反射

reflect.TypeOf vs reflect.ValueOf

  • reflect.TypeOf 返回的是类型(reflect.Type)
  • reflect.ValueOf 返回的是值(reflect.Value)
  • 可以从 reflect.Value 获取类型
  • 通过 kind 来判断类型
func CheckType(v interface{}) {
  t := reflect.TypeOf(v)
  switch t.Kind() {
  case reflect.Int, reflect.Int32, reflect.Int64:
    fmt.Println("integer")
  case reflect.Float32, reflect.Float64:
    fmt.Println("float")
  default:
    fmt.Println("unknown")
  }
}
func TestBasicType(t *testing.T) {
  var f int = 12
  CheckType(f)
}
func TestTypeAndValue(t *testing.T) {
  var f int64 = 12
  t.Log(reflect.TypeOf(f), reflect.ValueOf(f))
  t.Log(reflect.ValueOf(f).Type())
}

利用反射编写灵活的代码

  • 按名字访问结构的成员:reflect.ValueOf(*e).FieldByName("Name")
  • 按名字访问结构的方法:reflect.ValueOf(e).MethodByName("UpdateAge").Call([]reflect.Value{reflect.ValueOf(xxx)})

reflect.ValueOf 和 reflect.TypeOf 都有 FieldByName 方法,区别:

  • reflect.ValueOf 返回一个值
  • reflect.TypeOf 返回两个值
type Employee struct {
  EmployeeID string
  Name       string `format:"normal"`
  Age        int
}
func (e *Employee) UpdateAge(newVal int) {
  e.Age = newVal
}
type Customer struct {
  CookieID string
  Name     string
  Age      int
}
func TestInvokeByName(t *testing.T) {
  e := &Employee{"1", "Mike", 30}
  fmt.Printf("Name: value(%[1]v), Type(%[1]T)\n", reflect.ValueOf(*e).FieldByName("Name"))

  if nameField, ok := reflect.TypeOf(*e).FieldByName("Name"); !ok {
    t.Error("Failed to get 'Name' field.")
  } else {
    fmt.Println("Tag:format", nameField.Tag.Get("format"))
  }

	reflect.ValueOf(e).MethodByName("UpdateAge").Call([]reflect.Value{reflect.ValueOf(1)})
  fmt.Println("Updated Age:", e)
}

比较 map 和 slice

两个 map 是不能直接进行比较的,使用 reflect.DeepEqual 来进行比较(slice 同理)

func TestDeepEqual(t *testing.T) {
  a := map[int]string{1: "one", 2: "two", 3: "three"}
  b := map[int]string{1: "one", 2: "two", 3: "three"}
  fmt.Println(a == b) // 报错
  fmt.Println(reflect.DeepEqual(a, b))

  s1 := []int{1, 2, 3}
  s2 := []int{1, 2, 3}
  s3 := []int{2, 3, 1}
  fmt.Println(reflect.DeepEqual(s1, s2))
  fmt.Println(reflect.DeepEqual(s1, s3))
}

往期文章

  1. go 项目ORM、测试、api文档搭建
  2. go 开发短网址服务笔记
  3. go 实现统一加载资源的入口
  4. go 语言编写简单的分布式系统
  5. go 中 rpc 和 grpc 的使用
  6. protocol 和 grpc 的基本使用
  7. grpc 的单向流和双向流
  8. GORM 基本使用
  9. gin 基本使用