go 基础知识

227

应用入口

  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 slicelen 函数返回的是它所包含的 byte 数,这个 byte 和字符是不一样的
  3. stringbyte 数组可以存放任何数据
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. intint8int16int32int64
  4. uintuint8uint16uint32uint64uinttptr
  5. byte:alias for uint8
  6. rune:alias for int32, represents a Unicode code point
  7. float32float64
  8. complex64complex128

类型转换

  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 后面再指定其他返回值,会覆盖掉 sumproduct

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. 所有参数都是值传递:slicemapchannel 会有传引用的错觉
  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 对应的结构体会被复用

指针

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

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

i := 1

  • iPtr := &iiPtr 的值是 i 的地址
  • iPtr := *&iiPtr 的值是 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)
}

关联

celsiuskelvin 关联起来了

预声明的类型是不能进行关联的,比如 intfloat64

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 被执行前,所有依赖的 packageinit 方法都会被执行
  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}),输出 keyvalue,并且带有类型
  9. fmt.Printf("Name: value(%[1]v), Type(%[1]T)\n", "uccs"),表示都是用第一个值

组合和转发

  1. report 是由 temperaturelocation 组合得到的
  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() 调用的是 temperatureaverage 方法,这就实现了转发

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

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

嵌入

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

使用嵌入,go 会自动将 temperaturelocation 的方法都加到 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 中有 temperaturelocation 都有 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

rangelenappend 等都可以正常处理值为 nilslice

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 和值为 nilslice 不相等,但它们可以替换使用

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,如果 okfalse,那么通道已经关闭

方法一

  1. sourceGopher 函数负责向 downstream 通道发送数据
    • 如果没有数据了,就向通道中发送空字符串
  2. filterGopher 函数负责过滤 upstream 通道中的数据
    • item 接收 upstream 通道中的数据
    • 无限循环,如果 item"" 跳出循环
    • 过滤掉通道中包含 "bad" 的字符串后赋值给 downstream
  3. printGopher 函数负责打印 upstream
    • v 接收 upstream 通道中的数据
    • 无限循环,如果 item"" 跳出循环
    • 打印 v
  4. main 函数负责调用 sourceGopherfilterGopherprintGopher 函数
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 函数返回一个通道,该通道在指定事件后会接收到一个值(发送该值的 goroutinego 运行时的一部分)

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

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)
}

WaitGrouptime.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.ValueOfreflect.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 基本使用