Go学习(通用)

127 阅读9分钟

Go 的特点

  1. 并发下的执行体之间的通信 go 关键字

  2. 统一代码风格

    1. public/private首字母大小写
    2. 花括号位置的约束
    3. 错误处理 defer关键字
    4. ...
  3. 编程风格

    1. 反对继承 支持struct类型组合
    2. 反对函数、操作符重载
    3. 放弃构造函数
    4. 提供非侵入式 接口 松耦合

Go语言特性

  1. 垃圾自动回收

  2. 内置高级类型 map、slice(可动态增长的数组)

  3. 允许多返回值

  4. 支持匿名函数

    1. f := func (x, y int) int {
          return x + y
      }
      
  5. 类型和接口

    1. type Bird struct{
      
      }
      func (b *Bird) Fly(){
          fmt.Print("flying ")
      }
      //可以在任何地方定义IFly接口
      type IFly interface{
          Fly()
      }
      func main(){
          //Bird类在实现的时候并没有声明与接口IFly的关系,但是接口和类型可以直接转换
          var fly IFly = new (Bird)
          fly.Fly()
      }
      
  6. 并发编程 在函数调用前使用关键字go 可以让函数以goroutine协程的方式进行执行

Go语言通过系统的线程来多路派遣这些函数的执行,使得 每个用go关键字执行的函数可以运行成为一个单位协程。当一个协程阻塞的时候,调度器就会自 动把其他协程安排到另外的线程中去执行,从而实现了程序无等待并行化运行。而且调度的开销 非常小,一颗CPU调度的规模不下于每秒百万次,这使得我们能够创建大量的goroutine,从而可 以很轻松地编写高并发程序

package main 
import "fmt" 
func sum(values [] int, resultChan chan int) {
    sum := 0 
    for _, value := range values { 
        sum += value 
    } 
    resultChan <- sum // 将计算结果发送到channel中 
} 
func main() {
    values := [] int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 
    resultChan := make(chan int, 2)
    go sum(values[:len(values)/2], resultChan)
    go sum(values[len(values)/2:], resultChan)
    sum1, sum2 := <-resultChan, <-resultChan // 接收结果 
    fmt.Println("Result:", sum1, sum2, sum1 + sum2)
}

Go的变量、数据类型

  1. 变量声明

    1. Var 关键字 + 变量名 + 变量类型
    2. var v1 int/string/[10] int/ *int
      
      //若干个需要声明的变量放在一起
      var {
          v2 map[string]int
          v3 func(a int) int //接收int类型数据,int返回值类型的函数
      }
      
    3. 变量初始化 var关键字可以保留,但不是必要
    4. var v4 int = 10
      var v5 = 10 // 编译器自动推断类型
      v6 := 10 //:= 表达同时进行变量声明和初始化, 要求左侧的变量不应该是被声明过的,否则导致编译错误
      
    5. 变量赋值
    6. i, j = j, i //支持多重赋值
      
    7. 匿名变量 _的使用
    8. 在调用函数时为了获取一个 值,却因为该函数返回多个值而不得不定义一堆没用的变量。在Go中这种情况可以通过结合使 用多重返回和匿名变量来避免这种丑陋的写法,让代码看起来更加优雅

  2. 类型

    1. 布尔类型: bool

    2. 整型:int8, byte, int16, int32,uint, uintptr

    3. 对于常规的开发来说,用int 和uint就可以了,没必要用int8之类明确指定长度的类型,以免导致移植困难

    4. 浮点型:float32, float64

    5. 复数:complex64/128

    6. 字符串:string

      1. 字符串拼接 a+b
      2. len (string) 获取长度
      3. string[i] 取字符
      4. 字符串遍历
      5. //法一 以字节数组的方式遍历
        str := "Hello, world"
        n := len(str)
        for i := 0; i < n; i++{
            ch := str[i] // 类型为byte
            fmt.Println(i, ch)
        }
        //法二 以unicode字符遍历
        for i, ch := range str{
            fmt.Println(i, ch) // ch类型为rune
        }
        
    7. 字符:rune

    8. 错误类型:error

    9. 复合类型:pointer, array, slice, map, chan, struct, interface

  1. 数据切片

    1. 基于数组创建

    2. 直接make函数创建

    3. var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
      
      var mySlice []int = myArray[:5]
      
      mySlice1 := make([]int, 5) //创建一个初始元素个数为5的数组切片,元素初始值为0
      mySlice2 := make([]int, 5, 10) // 容量为10 
      
    4. 动态删减元素

      1. 可动态增减元素是数组切片比数组更为强大的功能。与数组相比,数组切片多了一个存储能 力(capacity)的概念,即元素个数和分配的空间可以是两个不同的值。合理地设置存储能力的 值,可以大幅降低数组切片内部重新分配内存和搬送内存块的频率,从而大大提高程序性能

      2. cap() 返回数组切片分配的空间大小
      3. len() 返回数组切片当前所存储的元素个数
      4. append()从尾端添加元素,生成新的切片
      5. mySlice = append(mySlice, 1, 2, 3) 
        //函数append()的第二个参数其实是一个不定参数,我们可以按自己需求添加若干个元素, 
        //甚至直接将一个数组切片追加到另一个数组切片的末尾:
        mySlice2 := []int{8, 9, 10}
         // 给mySlice后面添加另一个数组切片
         mySlice = append(mySlice, mySlice2...) 
        //需要注意的是,我们在第二个参数mySlice2后面加了三个点,即一个省略号
        //如果没有这个省 略号的话,会有编译错误
        //因为按append()的语义,从第二个参数起的所有参数都是待附加的 元素
        //因为mySlice中的元素类型为int,所以直接传递mySlice2是行不通的
        //加上省略号相 7 当于把mySlice2包含的所有元素打散后传入。 
        //上述调用等同于: 
        mySlice = append(mySlice, 8, 9, 10)
        
      6. copy() 复制
      7. slice1 := []int{1,2,3,4}
        slice2 := []int{1,2}
        copy(slice2, slice1) //复制slice1前两个元素到是slice2中
        copy(slice1, slice2) //复制slice2到slice1的前两个位置上
        
  2. map

    1. 声明

    2. 创建

    3. 赋值

    4. 删除

    5. 查找

      1. 声明并初始化一个变量为空;
      2. 试图从map中获取相应键的值到该变量中;
      3. 判断该变量是否依旧为空,如果为空则表示map中没有包含该变量
    6. type PersonInfo struct{
          ID string
          Name string
          Address string
      }
      func main(){
          //1.声明
          var myMap map[string] PersonInfo
          //2.创建
          myMap = make(map[string] PersonInfo)
          myMap = make(map[string] PersonInfo, 100) //制定100存储能力的map
          //3.赋值
          myMap["12345"] = PersonInfo{"12345", "Tom", "Room 203,..."} 
          myMap["1"] = PersonInfo{"1", "Jack", "Room 101,..."} 
          //4.删除
          delete(myMap, "12345"//5.查找
          person, ok := myMap["12345"]
          if ok {
              fmt.Println("Found person", person.Name, "with ID 1234.")
          } else {
              fmt.Println("Did not find person with ID 1234.") 
          } 
      }
      
  3. 接口 (方法声明的集合)

Instead of designing our abstractions in terms of what kind of data our types can hold, we design our abstractions in terms of what actions our types can execute

package main 
import (
    "fmt"
    "math"
)

type geometry interface {
    area() float64 // 面积
    perim() float64 // 周长
}
type rect struct{ // 矩形类
    weight, height float64
}
type circle struct{ // 圆类
    radius float64
}
// 要在 Go 中实现一个接口,我们只需要实现接口中的所有方法
// 这里我们为 rect 跟 circle 类 实现了 geometry 接口
func (r rect) area() float64 {
    return r.widgth * r.height
}
func (r rect) perim() float64{
    return 2 * (r.width + r.height) 
}
func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}

func measure (g geometry){
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}

func main(){
    r := rect{width: 3, height: 4}
    c := circle{radius : 5}
    // rect 类跟 circle 类都实现的geo接口 (感觉可以理解成java里的子类的概念)
    // 所以可以将其实例 作为measure 的参数
    measure(r)
    measure(c)
}
  1. 错误处理

    1. 基本模式
    2. func Foo(param int)(n int, err error){
      }
      n, err = Foo(0)
      if err != nil{
          // 错误处理
      }else{
          // 使用返回值n
      }
      
    3. 自定义error类型
    4. // 1. 错误类
      type PathError struct {
          Op string
          Path string
          Err error
      }
      // 2. 实现Error()方法
      func (e *PathError) Error() string{
          return e.Op + "" + e.Path + ":" + e.Err.Error()
      }
      
  2. 理解defer、panic和recover

    1. defer将一个函数放入一个列表(用栈表示其实更准确, 后入先出)中,该列表的函数在环绕defer的函数返回时会被执行。defer通常用于简化函数的各种各样清理动作,例如关闭文件,解锁等等的释放资源的动作。
    2. func CopyFile(dstName, srcName string) (written int64, err error) {
          src, err := os.Open(srcName)
          if err != nil {
              return
          }
          defer src.Close()
      
          dst, err := os.Create(dstName)
          if err != nil {
              return
          }
          defer dst.Close()
      
          return io.Copy(dst, src)
      }
      
    3.   在每个资源申请成功的后面都加上defer自动清理,不管该函数都多少个return,资源都会被正确的释放,例如上述例子的文件一定会被关闭。
    4. Panic是内置的停止控制流的函数。相当于其他编程语言的抛异常操作。当函数F调用了panic,F的执行会被停止,在F中panic前面定义的defer操作都会被执行,然后F函数返回。对于调用者来说,调用F的行为就像调用panic(如果F函数内部没有把panic recover掉)。如果都没有捕获该panic,相当于一层层panic,程序将会crash。panic可以直接调用,也可以是程序运行时错误导致,例如数组越界。
    5. Recover是一个从panic恢复的内建函数。Recover只有在defer的函数里面才能发挥真正的作用。如果是正常的情况(没有发生panic),调用recover将会返回nil并且没有任何影响。如果当前的goroutine panic了,recover的调用将会捕获到panic的值,并且恢复正常执行。
package main

import (
    "errors"
    "fmt"
)

func f1(arg int) (int, error) {
    if arg == 42 {
           
        //errors.New 使用给定的错误信息构造一个基本的 error 值。
        return -1, errors.New("can't work with 42")

    }

    return arg + 3, nil
}

//通过实现 Error() 方法来自定义 error 类型
type argError struct {
    arg  int
    prob string
}

func (e *argError) Error() string {
    return fmt.Sprintf("%d - %s", e.arg, e.prob)
}

func f2(arg int) (int, error) {
    if arg == 42 {
        //使用 &argError 语法来建立一个新的结构体, 并提供了 arg 和 prob 两个字段的值
        return -1, &argError{arg, "can't work with it"}
    }
    return arg + 3, nil
}

func main() {

    for _, i := range []int{7, 42} {
        //在 if 的同一行进行错误检查,是 Go 代码中的一种常见用法
        if r, e := f1(i); e != nil {
            fmt.Println("f1 failed:", e)
        } else {
            fmt.Println("f1 worked:", r)
        }
    }
    for _, i := range []int{7, 42} {
        if r, e := f2(i); e != nil {
            fmt.Println("f2 failed:", e)
        } else {
            fmt.Println("f2 worked:", r)
        }
    }

    _, e := f2(42)
    if ae, ok := e.(*argError); ok {
        fmt.Println(ae.arg)
        fmt.Println(ae.prob)
    }
}

Go的并发编程

func Add(x, y int){
    z := x + y
    fmt.Println(z)
}
// go调用的函数有返回值,那么这个返回值会被丢弃
go Add(1,1)
// 线程之间通信 线程还未执行 主程序已经结束
func main(){
    for i := 0; i < 10; i++{
        go Add(1,1)
    }
}
  1. 支持协程 goroutine,轻量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量

“不要通过共享内存来通信,而应该通过通信来共享内存。”

  1. 消息通信机制 channel

    1. Channel 的声明 在类型前加了chan关键字

      1. var ch chan int (声明了一个传递类型为int的channel)
      2. var m map[string] chan bool
    2. 声明并初始化

      1. ch := make(chan int)
      2. ch := make(chan int, 1024) 给channel带上缓冲,达到消息队列的效果,即1024的缓冲区空间
    3. 通道写入和读出 通过 -> <- 箭头指向流向

      1. ch <- value 向channel写入数据通常会导致程序阻塞,直到有其他goroutine从这个channel中读取数据
      2. value := <-ch 从channel中读取数据,如果channel之前没有写入数据,那么从channel中读取数据也会导致程序阻塞,直到channel中被写入数据为止
    4. 超时机制 可以利用select机制实现

    5. //select 函数中, 每一个case块都必须是一个IO(面向channel的操作)
      select {
          case <-chan1: 
      // 如果chan1成功读到数据,则进行该case处理语句 
          case chan2 <- 1:
       // 如果成功向chan2写入数据,则进行该case处理语句 
           default: 
      // 如果上面都没有成功,则进入default处理流程 
      } 
      
    6. timeout := make(chan bool, 1)
      go func (){
          time.Sleep(1e9) // 等待1秒钟
          timeout <- true
      }()
      select {
          case <- ch:
          case <- timeout:
          //一直没有从ch中读到数据,但是从timeout中读到
          //timeout 中保证 等待1s中必有一个true写入, 避免永久等待
      }
      
    7. 单向channel

    8. 设计的角度考虑,所有的代码应该都遵循“最小权限原则”, 从而避免没必要地使用泛滥问题,进而导致程序失控 。与声明为常量的目的类似,将一个指针设定为const就是明确告诉函数实现者不要试图对该指针进行修改。单向channel也是起到这样的一种契约作用

    9. // 1. 单向通道的声明
      var ch1 chan int 正常的channel
      var ch2 chan<- float64 只允许float64数据流入,即单向写float64数据
      var ch3 <-chan int 单向读取int数据
      // 2. 单向通道的初始化 可以进行双向2单向的类型转换
      ch4 := make(chan int)
      ch5 := <-chan int(ch4)
      ch5 := chan<- int(ch4)
      
    10. 关闭channel

    11. close(ch) 
      
      // 如何判断一个channel是否已经被关闭
      x, ok := <- ch 与查询map类似,看第二个bool返回值即可,如果为false,即表示已经被关闭
      
  2. 同步锁 sync.Mutex 跟sync.RWMutex(单写多读模型)

var lock sync.Mutex
func foo(){
    lock.Lock()
    defer lock.Unlock()
}

Go的实战编程