Golang的诞生及优势

Golang语言发布于2009年,是一门由Google支持的开源的静态编译型语言。作者是3位资深的语言专家:Robert GriesemerRob PikeKen Thompson。综合了各流行语言的优势,总体很接近C语言体系,主要优点如下:

  • 简单易学
  • 开发速度快
  • 性能好
  • 并发支持好

Docker、Kubernetes(K8s)、Etcd、Consul、Codis、Tidb等优秀的项目都是基于Golang开发的

容器:数组、切片、映射

数组(Array)

类型定义

数组的类型,取决于数组中的元素类型和数组的长度,其中数组的长度不可变;

[N]TN表示数组的长度,T表示数组中的元素类型,比如[10]int

var num1 [10]int
var num2 [5]int
var num3 [5]int32

reflect.Typeof(num1) == reflect.Typeof(num2)

数组变量声明及初始化

第二种方式只能在函数内使用

var num1 [3]int   // num1 := [3]int{}
num2 := [3]int{1,2,3}
num3 := [...]int{1,2,3}

数组类型是值类型,不是引用类型, 数组类型是值类型,不是引用类型

func main() {
    var data [3]int 
    var data2 [2]string
    data3 := [...]string{"hello","world"}

    fmt.Println(reflect.TypeOf(data) == reflect.TypeOf(data2))  // 1
    fmt.Println(reflect.TypeOf(data2) == reflect.TypeOf(data3))  // 2

    initNum(data) 
    fmt.Println(data) // 4 [0 0 0]
}

func initNum(num [3]int) {
    for i := 0; i < len(num); i++ {
        num[i] = i
    }
    fmt.Println(num)  // 3 [0 1 2] 
}

切片(Slice)

类型定义

  • []TT表示切片中的元素类型
  • 切片包含了对一段儿底层数组的动态引用,以及长度和容量
  • 长度表示已经有几个元素、容量表示一共能放多少元素
type Slice struct {
   Data unsafe.Pointer
   Len  int
   Cap  int
}

切片变量定义的一般方法

使用make函数,make的参数含义

var aa []int
aa = make([]int, 4) // 长度和容量都是4
bb := make([]int, 4, 6)

切片变量定义的其他方法

直接从已知数组或者切片中得到,注意越界问题

var num = [10]int{0,1,2,3,4,5,6,7,8,9}
slice1 := num[2:5:8]   // [2,3,4], len=3, cap=6
slice2 := num[2:5] // 及其他写法 cap=8
slice3 := num[2:] 
slice4 := num[:5]  // [0 1 2 3 4]
slice5 := num[:]

注意避免两个slice相互影响,例如:

slice1 := []int{1,2,3,4,5,6}
slice2 := slice1[1:4]
slice3 := slice1[2:6] 
slice2[3] =6

更推荐的方法

func copySlice() {
    slice1 := []int{1,2,3,4,5,6}

    slice2 := make([]int, 3)
    copy(slice2,  slice1[1:4])  // copy(dst, src)

    slice3 := make([]int, 4)
    copy(slice3,  slice1[2:6])
}

func some() {
    slice1 := make([]int, 0, 2)
    slice1 = append(slice1, 1,2,3)
}

常用函数append(slice, ...), len(slice), cap(slice), copy(dst, src)

切片的扩容

  • 当切片长度等于容量时,再向切片中添加元素,会引发切片扩容
  • 容量在1024以下,扩容每次乘2;否则,每次容量乘1.25
  • 发生扩容时,Go就会开辟一块新的内存,把原来的值拷贝过来

下面的wordslen(words)cap(words)是多少?

func main() {
   words := make([]string, 1, 3)
   words = append(words, "word1", "word2") // 3 3
   words = append(words, "word3") // 4 6
}

看个例子

func main() {
    var num = [10]int{0,1,2,3,4,5,6,7,8,9}
    slice := num[2:5:8] // [2 3 4] 3 6

    doubleNum(slice) // 传递的是指针
    fmt.Println(slice) // 2 [4 6 8]
    doubleNum2(slice)
    fmt.Println(slice) // 4
}

func doubleNum(slice []int) {
    for i := 0; i < len(slice); i++ { // 注意,这里不能用cap(slice)
        slice[i] = slice[i] * 2
    }
    fmt.Println(slice) // 1   [4 6 8]
}

func doubleNum2(slice []int) {
   for i, n := range slice {
      n = n * 2  // 注意for-range是值复制,不会对slice产生实际影响
      slice[i] = slice[i] * 2
   }
   fmt.Println(slice) // 3
}

映射

  • 映射的定义:map[K]T
  • key、value结构
  • delele,删除元素
func main() {
    students := make(map[string]int)
    fmt.Println(students, len(students))
    students["Michael"] = 12
    students["Tom"] = 10
    students["Jack"] = 11
    fmt.Println(students, len(students)) // map[Jack:11 Michael:12 Tom:10] 3
    delete(students, "Jack")
    fmt.Println(students, len(students)) // map[Michael:12 Tom:10] 2

    for k, v := range students {
    }
}

Golang没有内置集合(Set)类型,用Map来实现Set, 数组、切片、映射这几个容器都不是并发安全的

结构体与接口

结构体(Struct)

struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套其他结构体,也可以定义方法.

结构体的定义

  • 如何实现封装? 首字母大写或者小写
  • 如何实现继承? 可以用结构体组合的方式
  • 结构体的tag
type Gender int

const (
    Female Gender = 0
    Male   Gender = 1
)

type Person struct {
    Name string `json:"person_name"`
    Age int32
    Sex Gender
}

结构体的方法, 方法与函数的区别

func (p Person) ShowInfo() {
    if p.Sex == Female {
        fmt.Printf("Her name is %s and she is %d ages old", p.Name, p.Age)
       }else {
        fmt.Printf("His name is %s and he is %d ages old", p.Name, p.Age)
    }
}

对结构体的初始化方法

  • 直接初始化
  • 使用new初始化person := new(Person),返回的是指针
  • newmake的区别
func main() {
   person := Person{
      Name : "Michael",
      Age : 17,
      Sex : Male,
   }

   person2 := &Person{
      Name : "Michael",
      Age : 17,
      Sex : Male,
   }

   person3 := new(Person)
   person3.Name = "Michael"
   person3.Age = 17
   person3.Sex = Male

   person.Name = "Tom"
}

结构体方法的使用

func (p Person) ShowInfo() {
    if p.Sex == Female {
        fmt.Printf("Her name is %s and she is %d ages old", p.Name, p.Age)
    }else {
        fmt.Printf("His name is %s and he is %d ages old", p.Name, p.Age)
    }
}
func (p *Person) SetAge(age int32) {
    p.Age = age
}

func main() {
   person := Person{
      Name : "Michael",
      Age : 17,
      Sex : Male,
   }
   person.SetAge(18)
   person.ShowInfo()
}

接口(Interface)

接口描述了某个类型有哪些方法。或者说一个接口类型,定义了一个方法集。通过接口可以轻松实现多态

type Animal interface {
    Shout()
}

type Dog struct{}

type Cat struct{}

func (d Dog) Shout() {
    fmt.Println("汪汪")
}

func (c Cat) Shout() {
    fmt.Println("喵")
}

空接口可以被认为是很多其它语言中的any类型, 空接口中没有任何方法,所以任何类型都实现了空接口, Println为啥什么都能打印

func Println(a ...interface{}) (n int, err error)

类型断言

Plaintext
func main() {
   // 编译器将把123的类型推断为内置类型int。
   var x interface{} = 123

   // 情形一:
   n, ok := x.(int)
   fmt.Println(n, ok) // 123 true
   n = x.(int)
   fmt.Println(n) // 123

   // 情形二:
   a, ok := x.(float64)
   fmt.Println(a, ok) // 0 false

   // 情形三:
   a = x.(float64) // 将产生一个恐慌
}

协程与管道

协程(Goroutine)

Go不直接支持创建系统线程,协程是Go程序内部唯一的并发实现方式.

func main() {
    fmt.Println("主协程开始")
    go func() {
        time.Sleep(1 * time.Second)
        fmt.Println("打印日志")
        time.Sleep(1 * time.Second)
    }()
    time.Sleep(3 * time.Second)
    fmt.Println("主协程结束")
 }

注意,主协程(main)结束后,此程序也就退出了,即使还有一些其它协程在运行.

协程的底层是如何实现的?

  • 基于GMP模型
    • G: goroutines 表示一个协程
    • M: machine 表示一个线程
    • P:Processor 管理器,通过队列管理协程
  • 基于GMP模型,协程运行在线程上
  • 一个协程中的信息:运行栈+寄存器数值(PC、BP、SP)
    • 协程的切换,仅仅需要改变寄存器的数值,cpu便会从需要切换的协程指定位置继续运行
  • 协程与线程比例关系N:M
协程:线程 含义 优点 缺点
1:1 一个协程在一个线程运行 (其实就是传统的多线程) 利用多核 上下文切换比较慢,为什么?
N:1 多个协程在一个线程上运行 上下文切换较快 1、无法充分利用多核 2、饥饿,如果一个协程不结束,其余协程阻塞
N:M 多个协程在多个线程上运行 充分利用多核,上下文切换快

协程的调度器的设计策略(减少开销兼顾公平):

  • 复用线程(避免频繁的创建、销毁线程,而是对线程的复用)
    • work stealing机制:当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。
    • hand off机制:当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。
  • 利用并行:GOMAXPROCS设置P的数量
  • 抢占:限制协程执行时长,不会出现饿死现象
  • 全局协程队列:链表实现

常用的同步控制机制:WaitGroup

开发过程中,经常遇到多task之间的同步问题。例如,多个子task并发完成一部分任务,主task等待他们最后结束。类似于Java的CountDownLatch

func main() {
   var wg sync.WaitGroup ;
   for i := 0; i < 3; i++ {
      wg.Add(1)
      go func(i int) {
         t := rand.Intn(3)
         time.Sleep(time.Duration(t) * time.Second)
         fmt.Printf("The %d thing is Done.\n", i)
         wg.Done()
      }(i)
   }
   wg.Wait()
   fmt.Println("Finished!")
}

管道(Channe)

并发模型CSP,全称Communicating Sequential Processes。它的核心观念是将两个并发执行的实体通过管道连接起来,所有的消息都通过管道传输。

管道(通道),也是一种Golang的数据同步技术。它可以被看作是在一个程序内部的一个先进先出(FIFO:first in first out)数据队列。

  • 管道的操作有:读、写和关闭。
  • 定义:ch := make(chan string)
  • 读:a = <- ch
  • 写:ch <- "hello"
  • 写一个已经关闭的channel会引发Panic。

无缓冲管道:长度为0的channel,为不带buffer的channel

ch := make(chan int, 10)

  • 会发生额外的拷贝
  • 写在读前 ch <- 1
  • 缓冲区最大为65535

管道元素的传递,是复制,非缓冲区管道复制了1次,缓冲区管道复制了2次。

一个交替打印AB的例子:

import "fmt"

func main() {
   ch1 := make(chan string)
   ch2 := make(chan string)
   ch3 := make(chan string)
   go printA(ch1, ch2)
   go printB(ch1, ch2, ch3)
   <-ch3
}

func printA(ch1, ch2 chan string) {
   for i := 0; i < 100; i++ {
      <-ch2 
      fmt.Println(i, "A")
      ch1<- "print A"
   }
}

func printB(ch1, ch2, ch3 chan string) {
   ch2 <- "begin"  
   for i := 0; i < 100; i++ {
      <-ch1
      fmt.Println(i, "B")
      if i != 99 {
         ch2 <- "print B"
      }else {
         ch3 <- "end"
      }
   }
}