Go 学习笔记

作者: gothicrush | 来源:发表于2018-09-05 23:14 被阅读64次

    [内容][111]
    <span id="jump">Hello World</span>

    Part 1 开发环境

    环境变量设置

    GOROOT

    指定 golang sdk 的安装目录
    

    GOPATH

    golang 工作目录,项目的源码放在这个目录下
    

    PATH

    将 GOROOT/bin 放在 Path 路径下,方便命令行能直接运行 golang的命令行工具
    

    Go项目目录结构

    |--project                    // 位于GOPATH下
        |--src                    // 存放源代码
            |--packageA
                |--packageA.go
            |--packageB
                |--packageB.go
        |--pkg                    // 编译后生成的文件
        |--bin                    // 编译后生成的可执行文件
    

    Part 2 基础知识

    Go的注释

    • 行注释
    // 行注释
    
    • 块注释(多行注释)
    //块注释不可以嵌套
    
    /*
    Comment 1
    Comment 2
    Comment 3
    ...
    Comment n
    */
    

    Go的数据类型

    基本数据类型

    • 数值型

      • 整数型

        • int8,int16,int32,int64

        • uint8,uint16,int32,int64

        • --------------------------------------------------------------------------

        • byte:uint8的别名

        • rune:int32的别名

        • int:64位操作系统则int64,32位操作系统则int32

        • uint:64位操作系统则uint64,32位操作系统则uint32

      • 浮点型

        • float32,float64
      • 复数型

        • complex64,complex128
    • 布尔型

      • bool:占用1个字节,只能接受true和false
    • 字符串

      • string

    复杂数据类型

    • 数组 - 值类型
    • 结构体 - 值类型
    • 指针 - 引用类型
    • 管道 - 引用类型
    • 函数 - 引用类型
    • 接口 - 引用类型
    • 切片 - 引用类型
    • 映射 - 引用类型

    给类型起别名

    type myInt int // 此时 myInt 可以作为 int 使用
    
    • 虽然说是起别名,但是 编译器认为 myInt和int不是同一种类型,它们仅仅是底层数据结构相同

    Go的变量

    变量创建的方法

    // 方式1:指定变量类型
    var i int      // 声明变量         // 可以声明的同时初始化
    i = 10         // 为变量赋值       // 等价于 var i int = 10
    fmt.Println(i) // 使用变量
    
    // 方式2:使用类型推导 —— 必须显式初始化
    var i          // 声明变量         // 可以声明的同时初始化
    i = 10         // 为变量赋值       // 等价于 var i = 10
    fmt.Println(i) // 使用变量
    
    // 方式3:使用简洁语法 —— 不可以用于函数外的变量声明,只能用于局部变量
    i := 10        // 声明变量并由类型推导初始化
    fmt.println(i) // 使用变量
    
    // 方式4:同时创建多个全局变量 —— 只能用于函数外的变量声明
    var (                                      
        n1 = 100
        name = "gothicrush"
        n3 = 200
    )
    

    引用类型变量相关

    • 对于引用类型变量,声明后还不能使用,在为它分配内存空间后才能使用

    • 分配内存空间有两个函数

      • new(type)

        • 只接收一个参数,该参数为一个类型
        • 函数返回只想该类型内存地址的指针,该内存地址的值为类型零值
      • make(type, len, prelen)

        • 只适用于 slice, map, channel

        • prelen为预留长度,切片的预留长度需要重新切片后才能使用

          a := make([]int, 10, 20)
          b := a[:cap(a)]
          

    变量作用域

    • 全局作用域:函数外定义的变量,可以在包中或整个程序(大写开头)中使用
    • 局部作用域:函数内定义的变量,只能在函数内使用
    • 当函数内部的局部变量和全局变量重名时,编译器采用就近原则,即局部变量覆盖全局变量

    变量的注意事项

    • 变量声明后,系统自动为它初始化为默认值

      在go 中,数据类型都有一个默认值,当程序员没有赋值时,编译器默认给它赋一个默认值

      整数型 0
      浮点型 0.0
      复数型 0+0j
      布尔型 false
      字符型 ""
      复合类型 nil
      
    • 支持一行语句中声明多个变量

      var n1 int, n2 string, n3 int              //不推荐使用
      
      var n1, name, n3 = 100, "gothicrush", 200  //不推荐使用
      
      n1, name, n3 := 100, "gothicrush", 200     //不推荐使用
      
    • 查看变量的类型和占用字节大小

      fmt.Printf("%T", n1)
      fmt.Printf("%d", unsafe.Sizeof(n1))
      
    • golang的整型默认为int类型

      • 64位操作系统则int64
      • 32位操作系统则int32
    • 浮点数都是有符号的,golang的浮点型默认为float64

    • 浮点数表示方式

      var n1 float64 = 0.12
      var n2 float64 = .12
      var n3 float64 = 5.123e2
      
    • golang的字符使用utf-8编码,避免乱码的问题

      • 字母:1个字节
      • 汉字:3个字节
    • golang中没有专门的字符类型

      • 对于ascii编码,用byte存储
      • 对于utf-8编码,用rune存储
    • 字符常量用单引号括起来,字符类型可以参与运算,因为它本质上是整型

    • 在Go中字符串是不可变的,字符串有两种形式

      // 1. 双引号,会识别转义字符
      // 2. 反引号,原生输出,原来是什么,就是什么;反引号可以多行
      
    • 字符串拼接

      var str = "hello" + " world" +  //如果是多行,需要将 + 放在上面
                "多行"
      

    Go类型转换

    • 基本概念
    go中不存在自动(隐式)转换,只有显式转换,就算是低精度->高精度都要显式转换
    语法:T(value)
    var i int = 42
    var b float64 = float64(i)
    
    • 注意事项
      • 转换后返回一个新的,被转换的值本身没有变化
      • 如果转换过程中发生溢出(比如int64->int8),则不会报错,只是会按溢出处理

    基本数据类型与string转换

    使用 strconv 包
    
    • 基本数据类型转为string

      strconv.Itoa(i)
      
    • string转为基本数据类型

      b, err := strconv.ParseBool(str)
      i, err := strconv.ParseInt(str, 10, 64) // 10进制,int64
      f, err := strconv.ParseFloat(str, 64)   // float64
      
      如果转换失败,则返回为默认值,还有err
      

    Go标识符

    • 规则
    只能由 数字,英文字母和_组成
    
    不能以数字开头
    
    单独的 _ ,是一个特殊符号,不能用作标识符
    
    var _ int = 64 //error
    
    不能用保留关键字,int,float64 等等不是保留关键字,但不推荐使用
    
    • 命名规范

      要有意义,要尽量简短
      
      包名:保持package的名字和目录的名字相同
      
      变量名,函数名,常量名:采用驼峰命名法
      
    • 首字母大写是公有,首字母小写是私有

    预定义标识符和保留关键字

    • 程序声明:import、package
    • 程序实体声明和定义:chan、const、func、interface、map、struct、type、var
    • 程序流程控制:go、select、break、case、continue、default、defer、else、fallthrough、for、goto、if、range、return、switch

    Part 3 - 指针

    • 获取变量的地址(指针),用 & 操作符,比如 &number

    • 指针类型,存储的是一个地址,比如 *int,*float64

    • 访问指针类型指向空间,用 *,比如 *ptr

    • 指针空值类型是nil ,而不是 null

    • 不支持 ->,一律用 .

    • 不支持指针运算

    Part 4 - 包管理

    包的概述

    • Go中每一个文件都属于一个包,即Go是以包的形式来管理文件和项目目录结构的
    • 包的名字规范是全小写

    包的作用

    • 区分相同名字的标识符
    • 控制函数,变量的作用域
    • 对函数以及变量进行管理
    • 声明某个go文件是某个包

    声明包

    package db
    

    导入包

    • 从 src 目录起,写绝对路径

      // 单个导入
      import "go_code/chapter03/demo04/model"
      
      // 批量导入
      import (
          "go_code/chapter03/demo04/model_1"
          "go_code/chapter03/demo04/model_2"
          "go_code/chapter03/demo04/model_3"
      )
      
      // 如果包太长,可以给包改名,也可以避免重名的问题
      import model "go_code/chapter03/demo04/model"
      
    • 导入包可以写绝对路径和相对路径

      • 绝对路径:从 src 目录起,写绝对路径
      • 相对路径:以当前文件为基准点

    包的搜索路径

    • 标准库
    • GOPATH指定目录/src

    包的init函数

    • 一个包一旦被导入,就会自动执行这个包中的init函数
    • 一个包中可以有多个init函数
    • 可以配合 _ 来使用

    包的注意事项

    • 文件名和包名最好相同

    • 如果导入了包而不使用则会编译错误,如果只想执行包中的init函数而不想使用,则可以用 _

      import _ "packagename"
      
    • 如果想要定义一个go文件是可执行文件,则需要将这个文件所属包声明为main

      package main // 这是规范
      
    • 不能将多个包放在同一目录下,也不能将一个包拆分到多个目录下,总之一个包就是一个目录,这个目录名和包名相同

    • 对于main包中不同的文件的代码不能互相调用

    Part 5 - 运算符

    运算符分类

    • 算术运算符
    • 赋值运算符
    • 比较运算符
    • 逻辑运算符
    • 位运算符
    • 其他运算符

    算术运算符

    • 对数值类型使用
    • +,-,*,/,%,++,--
    • / :如果两边都是整数,则结果会把小数直接舍去
    • % :公式: a % b = a - a / b * b
    • ++/-- :只能作为独立的语句使用,即单独占一行,++和--只能写在变量的后面

    比较运算符

    • 用在条件判断或循环判断中,返回值是true或false
    • ==, >, < , >=, <=, !=

    逻辑运算符

    • 用在条件判断或循环判断中,参与运算的是true或false,返回值是true或false
    • &&(短路), ||(短路), !

    赋值运算符

    • =, +=, -=, *=, /=, %=

    • <<=, >>=, &=, |=, ^=

    • 赋值运算左边只能是变量,右边可以是变量,表达式,常量值

    • 交换变量而不使用中间变量

      var a int = 10
      var b int = 20
      
      a = a + b
      b = a - b // b = (a+b) - b = a
      a = a - b // a = (a+b) - a = b
      

    其他运算符

    • Go官方明确声明不支持 三元运算符
    • 取地址 &
    • 解指针 *

    Part 6 - IO

    从键盘获取数据

    import fmt
    
    // fmt.Scanln() // 换行时停止扫描
    
    var name string
    var age byte
    var sal float32
    var isPass bool
    
    fmt.Scanln(&name)
    fmt.Scanln(&age)
    fmt.Scanln(&sal)
    fmt.Scanln(&isPass)
    
    
    // fmt.Scanf()  // 扫描文本,根据format参数指定格式将读取空白分隔的值保存进本函数中
    var name string
    var age byte
    var sal float32
    var isPass bool
    
    fmt.Scanf("%s %d %f %t", &name, &age, &sal, &isPass)
    

    读取命令行参数

    • 方式一:通过 os.Args

      func main() {
          for index,val := range os.Args {
              fmt.Println(index, val)
          }
      }
      
    • 方式二:通过 flag 包

      import "flag"
      
      func main() {
          var user string
          var pwd string
          var host string
          var port int
          
          flag.StringVar(&user, "u", "","用户名,默认为空")
          flag.StringVar(&pwd, "pwd", "","密码,默认为空")
          flag.StringVar(&host, "h", "localhost", "主机名,默认为localhost")
          flag.StringVar(&port, "p", "3306", "端口号,默认为3306")
          
          flag.Parse()
      }
      

    文件的输入流与输出流

    什么是输入流/输出流

    • 文件在程序中是以的形式进行操作的

    • 输入流:数据从文件到内存

    • 输出流:数据从内存到文件

    文件操作的包

    • Go中文件操作的API都在 os 库中

    打开文件

    func os.Open(path string) (f *File, err error)
    

    关闭文件

    func (f *File) Close() error
    配合 defer 语句使用
    

    读文件

    • 带缓存读
    // 默认缓冲区大小为4096
    reader := bufio.NewReader(file)
    
    for {
        str,err := reader.ReadString('\n')
        
        if err == io.EOF {
            break
        }
    }
    
    • 一次性读取
    func ioutil.ReadFile(path string) (content []byte, err error) {}
    
    file := "/home/test.text"
    content, err := ioutil.ReadFile(file) // 不需要打开和关闭文件,这些操作都封装到ReadFile函数中了,适合小文件读取
    if err != nil {
        fmt.Println(string(content))
    }
    

    写文件

    func OpenFile(path string, flag int, perm FileMode) (file *File,err error)
    
    • 模式 flag,可以通过 | 组合使用
    O_RDONLY
    O_WRONLY
    O_RDWR
    O_APPEND
    O_CREATE
    O_EXCL
    O_SYNC
    O_TRUNC
    
    • 权限 perm
    r -> 4
    w -> 2
    x -> 1
    
    • 案例

      • 创建一个新文件,写入5句 "Hello World"

        file := "/home/abc.txt"
        file, err := os.OpenFile(file, os.O_WRONLY | os.O_CREATE, 0666)
        
        if err != nil {
            fmt.Println("open file successfully")
            return
        }
        
        defer file.Close()
        
        str := "hello, world"
        writer := bufio.NewWriter(file)
        
        for i := 0; i < 5; i++ {
            writer.WriterString(str)
        }
        
        // 因为带缓存,因此要刷新缓存
        writer.Flush()
        
      • 打开一个存在的文件,将原来的内容覆盖为新的内容,10句 "你好"

        file := "/home/abc.txt"
        file, err := os.OpenFile(file, os.O_WRONLY | os.O_TRUNC, 0666)
        
        if err != nil {
            fmt.Println("open file successfully")
            return
        }
        
        defer file.Close()
        
        str := "你好你好"
        writer := bufio.NewWriter(file)
        
        for i := 0; i < 10; i++ {
            writer.WriterString(str)
        }
        
        // 因为带缓存,因此要刷新缓存
        writer.Flush()
        
      • 打开一个文件,在原来的内容基础上追加内容 " Append"

        file := "/home/abc.txt"
        file, err := os.OpenFile(file, os.O_APPEND | os.O_TRUNC, 0666)
        
        if err != nil {
            fmt.Println("open file successfully")
            return
        }
        
        defer file.Close()
        
        str := " APPEND"
        writer := bufio.NewWriter(file)
        
        for i := 0; i < 10; i++ {
            writer.WriterString(str)
        }
        
        // 因为带缓存,因此要刷新缓存
        writer.Flush()
        
      • 打开一个存在的文件,将原来的内容读出显示在终端,并追加5句 "你好,世界"

        file := "/home/abc.txt"
        file, err := os.OpenFile(file, os.O_RDWR | os.O_APPEND, 0666)
        
        if err != nil {
            fmt.Println("open file successfully")
            return
        }
        
        defer file.Close()
        
        reader := bufio.NewReader(file)
        for {
            str, err := reader.ReadString("\n")
            if err == io.EOF {
                break
            }
            fmt.Println(str)
        }
        
        str := "你好,世界"
        writer := bufio.NewWriter(file)
        
        for i := 0; i < 5 i++ {
            writer.WriterString(str)
        }
        
        // 因为带缓存,因此要刷新缓存
        writer.Flush()
        
      • 将一个文件的内容,写入到另外一个文件(注意:这两个文件已经存在了)

        file1Path := "/home/abc.txt"
        file2Path := "/home/def.txt"
        
        data, err := ioutil.ReadFile(filePath)
        
        if err != nil {
            fmt.Println("文件读取失败")
            return
        }
        
        err = ioutil.Write(file2Path, data, 0666)
        
        if err != nil {
            fmt.Println(err)
        }
        

    判断文件与目录是否存在

    根据 os.Stat() 函数返回值进行判断
    1. 如果返回值为nil,说明文件或目录存在
    2. 如果返回值使用os.IsNotExist()判断为true,则说明文件或目录不存在
    3. 如果返回值为其他类型,则不确定是否存在
    
    func PathExisting(path string) (bool,error) {
        _, err := os.Stat(path)
        if err == nil {
            return true, nil
        }
        
        if os.IsNotExist(err) {
            return false, nil
        }
        
        return false, err
    }
    

    拷贝文件

    func io.Copy(dst Writer, src Reader) (Written int64, err error)
    
    func CopyFile(dstFilePath string, srcFilePath) (written int64, err error) {
        srcFile, err := os.Open(srcFilePath)
        
        if err != nil {
            fmt.Println("读取源文件错误")
            return
        }
        
        defer srcFile.Close()
        
        reader := bufio.NewReader(srcFile)
        
        dstFile, err := os.OpenFile(dstFilePath, os.O_WRONLY | os.O_CREATE, 0666) 
        
        if err != nil {
            fmt.Println("打开文件失败")
        }
        
        defer dstFile.Close()
        
        writer := bufio.NewWriter(dstFile)
        
        return io.Copy(writer, reader)
    }
    

    Part 7 - 流程控制

    分支流程控制

    • 单分支

      if 条件表达式 { // { } 必须要有,且左大括号不能换行
          //
      }
      
    • 双分支

      if 条件表达式 { // { } 必须要有,且左大括号不能换行
          
      } else {      // { } 必须要有,且左大括号不能换行,else必须和右大括号同行
           
      }
      
    • 多分支

      if 条件1 {          // { } 必须要有,且左大括号不能换行
          
      } else if 条件2 {   // { } 必须要有,且左大括号不能换行,else必须和右大括号同行
          
      } else if 条件n {   // { } 必须要有,且左大括号不能换行,else必须和右大括号同行
          
      } else {           // { } 必须要有,且左大括号不能换行,else必须和右大括号同行
                         // 多分支中 else 不是必须的
      }
      
    • if 语句可以初始化变量

      if ok := function(); ok {
          
      }
      
    • switch语句

      switch 表达式 {
          case 表达式1,表达式2,...:
              语句1
          case 表达式3:
            语句2
          default:
            语句
      }
      
      
      // case的表达式要求不能重复,case表达式值与switch表示值需要是同一类型
      // case后面可以跟多个表达式,用逗号分隔
      // 一旦匹配,就不再进行下一步匹配
      // default不是必须的,可有可无
      // 如果匹配不到,则执行default块(如果有)
      // 匹配选项后面不用加 break,默认不贯穿
      // 如果想要穿透,则用 fallthrough 声明
      // switch后也可以不跟表达式,效果等同 switch True {}
      // switch后可以先声明一个变量再使用 switch i:=100 {}
      
    • if-else if-else 与 switch 使用场景对比

      // 对于匹配区间
      推荐用 if-else if-else
      // 对于匹配具体值
      推荐用 switch
      

    循环分支流程控制

    • Go中只有 for 没有 while 也没有 do-while

    • for 使用方法-1 普通 for 循环

      for 循环变量初始化; 循环条件; 循环条件迭代 {
          // 循环操作语句
      }
      
    • for 使用方法-2 模拟 while 循环

      for 循环条件 {
          // 循环操作语句
      }
      
    • for 使用方法-3 死循环

      for {
          // 死循环
      }
      
      // 等价于
      for ;; {
          // 死循环
      }
      
    • for 使用方法-4 for-range

      for index,val := range str {
          fmt.Println(index, val)
      }
      
    • for使用方法-5 新形式

      for 初始化; 循环条件 {
          // 循环操作语句
      }
      
    • for 注意事项

      • 循环条件返回的 bool 的表达式

      • break 可以根据标签跳出

        break         //跳出最近的一层
        break label1  //跳到label1指定位置
        break label2  //跳到label2指定位置
        
        for i := 0; i < 4; i++ {
            label1:
            for j :=0; j < 10; j++ {
                if j == 2 {
                    break label1
                }
                fmt.Println("j =", )
            } 
        }
        
      • continue 可以根据标签跳出

        continue         //跳出最近的一层
        continuecontinue label1  //跳到label1指定位置
        break label2  //跳到label2指定位置
        
        for i := 0; i < 4; i++ {
            label1:
            for j :=0; j < 10; j++ {
                if j == 2 {
                    continue label1
                }
                fmt.Println("j =", )
            } 
        }
        

    goto语句

    • 不推荐使用 goto 语句

    • 语法

      label: 语句
      
      goto label
      

    Part 8 - 函数

    定义函数

    func 函数名 (形参列表) (返回值列表) { // 左大括号不能换行
        //执行语句
        return xxx //可有可无
    }
    

    函数参数简化

    func test(a int, b int, c int) {}
    等价于
    func test(a,b,c int) {}
    

    函数返回值

    • Go语言可以返回多个值,如果返回不想接收,可以用 _ 来省略

    • 如果只返回一个值,则返回值列表可以不加()

    • 不指定返回值名称

      
      
    • 指定返回值名称

      func add(a int, b int) (result int) {
          result = a + b
          return
      }
      

    可变参数

    • 会把不定参数封装为一个slice
    • 如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后
    func sum (args... int) total int {
        for _, val := range args {
            total += val
        }
        return
    }
    
    sum(1,2,3,4,5,6,7)
    

    函数注意事项

    • 基本数据类型和数组默认是值传递的,即进行值拷贝,在函数内部修改,不会影响到原来的值

    • 如果希望函数内部修改可以影响到外面,则需要使用指针

    • Go函数不支持函数重载,不支持嵌套,不支持默认参数

    • Go函数本身也是一种数据类型,可以将函数作为参数传递

      func getSum(n1 int,n2 int) int {
          return n1 + n2
      }
      
      func myFunc(funvar func(int,int) int, num int, num2 int) int {
          return funvar(num1, num2)
      }
      
    • 为函数类型起别名

      type myf func(int,int) int
      

    init函数

    • 名字固定,每一个Go源文件中都可以包含这样的一个函数,该函数在main函数执行前,会先被调用

    • 该函数可以用作初始化操作

      func init() { // 无形参,无返回值
          
      }
      
    • 如果有全局变量定义,则执行顺序为

      全局变量定义 -> init函数 -> main函数
      
    1.png

    匿名函数

    • 只调用一次的匿名函数

      res := func (n1 int, n2 int) int {
          return n1 + n2
      }(1,2)
      
    • 可调用多次的匿名函数

      a := func (n1 int, n2 int) int {
          return n1 + n2
      }
      
      a(1,2)
      a(3,4)
      a(5,6)
      
    • 全局匿名函数

      var (
          gFun = func(n1 int, n2 int) int {
              return n1 + n2
          }
      )
      

    闭包

    闭包就是 一个函数A中定义另外一个函数B,且函数B中有使用到函数A中的局部变量,且函数A将函数B返回
    

    defer

    • 作用

      defer是函数的延时执行机制
      defer语句当函数结束时才调用
      defer语句一般用于释放资源
      
    • 注意

      多个defer语句会放到一个栈中
      即最先声明的defer语句会在最后执行
      当语句入栈时,也会将相关的值进行拷贝
      

    函数参数传递方式

    • Go语言一种有两大类数据类型:值类型 ;引用类型
    • Go中值类型包括:int系列,float系列,complex系列,bool,string,数组和结构体
    • Go中引用类型包括:slice,map,chan,interface,指针
    • 值类型的默认传递方式是值传递,引用类型的默认传递方式是引用传递

    内置函数

    • len:用来求长度,如string,array,slice,map,channel

    • new:用来分配内存,主要用于分配值类型

      numPtr := new(int) 这个numPtr是 *int 类型
      
    • make:用来分配内存,主要用于分配引用类型

    • cap:求切片的容量

    Part 9 - 字符串常用操作

    • len(str):返回字符串的长度(按字节,字母1个字节,汉字3个字节)

    • 遍历字符串

      r := []rune(str)
      for i := 0; i < len(r); i++ {
          fmt.Printf("%c\n",r[i])
      }
      
    • 字符串转整数

      import strconv
      
      n, err := strconv.Atoi("123")
      
    • 整数转字符串

      import strconv
      
      n := strconv.Itoa(123)
      
    • 字符串转[]byte

      var n []byte = []byte("hello")
      
    • []byte转字符串

      str := string([]byte("123456"))
      
    • 10进制转2,4,8,16进制

      str := strconv.FormatInt(132,2)
      str := strconv.FormatInt(132,4)
      str := strconv.FormatInt(132,5)
      str := strconv.FormatInt(132,16)
      
    • 查找字符串中有无特定子串

      import strings
      
      b := strings.Contains("seafood", "food")
      
    • 统计字符串中有几个特定子串

      import strings
      
      c := strings.Count("ceeesseeee","e")
      
    • 查找子串在字符串从左到右第一次出现的下标值,没有则返回-1

      index := strings.Index("NTL_abc", "abc")
      
    • 查找子串在字符串从右到左第一次出现的下标值,没有则返回-1

      index := strings.LastIndex("NTL_abc", "abc")
      
    • 如果字符串中有特定子串,则将其换为其他子串

      str := strings.Replace("go hello", "go", "python")
      
    • 字符串按某个子串进行分隔,返回slice

      sl := strings.Split("hello-world-ok","-")
      
    • 将字符串进行大小写转换

      str := string.ToLower("HELLO")
      str := string.ToHigher("hello")
      
    • 字符串去空格

      去除左右两边的空格,strings.TrimSpace("  1  2  ")
      去除左右两边特定子串,strings.Trim("! 1 2 !", "!")
      去除左边特定子串,strings.TrimLeft("! 1 2 !", "!")
      去除右边特定子串,strings.TrimRight("! 1 2 !", "!")
      
    • 判断字符串是否以某个子串开头/结尾

      strings.HasPrefix("NLT_abc","NLT")
      strings.HasSuffix("NLT_abc","abc")
      

    Part 10 - 时间和日期常用操作

    • 相关包

      import time
      
    • 获取当前时间

      now := time.Now()
      
    • 获取当前年月日,时分秒

      now := time.Now()
      now.Year()
      now.Month()
      now.Day()
      now.Hour()
      now.Minute()
      now.Second()
      
    • 日期时间格式化

      // 方法1
      "%d/%d/%d %d:%d:%d",now.Year(),now.Month(),now.Day(),now.Hour(),now.Minute(),now.Second()
      
      // 方法2
      now.Format("2006/01/02 15:04:05")
      
      "2006/01/02 15:04:05" 这个时间固定的,必须这么写
      "2006/01/02 15:04:05" 中各个数字可以自由组合,这样可以格式化输出时间
      
    • 时间常量

      时    time.Hour
      分    time.Minute
      秒    time.Second
      毫秒  time.Millisecond
      微妙  time.Microsecond
      纳秒  time.Nanosecond
      
    • 休眠

      time.sleep(10 * time.Millisecond)
      
    • 获取当前时间戳

      time.Now().Unix()       // 从1970年到现在所经过的时间(单位秒),类型int64
      time.Now().UnxiNano()   // 从1970年到现在所经过的时间(单位纳秒),类型int64
      

    Part 11 - 错误处理机制

    panic-defer-recover机制

    • 默认情况下,当发生错误(panic)后,程序就会崩溃
    • 如果希望当发生 panic 后,可以捕捉到 panic,并进行处理,使程序不崩溃,则需要进行错误处理
    • Go语法优雅,不支持传统的 try...catch....finally,而是使用 defer, panic, recover机制
    • 流程:抛出一个panic,然后在 defer 语句中通过 recover 函数捕获这个 panic,并处理
    func main() {
       
       test()
       fmt.Println("尽管发生了 panic,程序还是继续执行了")
       
    }
    
    func test() {
       defer func() {
           err := recover() // recover内置函数,可以捕获异常
           if err != nil {
               fmt.Println("err=",err)
           }
       }()
       
       num1 := 10
       num2 := 0
       res := num1 / num2
       fmt.Println(res)
    }
    

    自定义异常

    • 使用errors.New和panic内置函数

      • error.New("错误说明"),返回一个 error 类型的值,表示一个错误
      • panic(),内置函数,接收任意值作为参数,引发异常
      import "errors"
      
      func readConf(name string) (err error) {
          if name == "config.ini" {
              return nil
          } else {
              return errors.New("读取配置文件错误")
          }
      }
      
      func test() {
          err := readConf("config.ini")
          if err != nil {
              panic(err)
          }
      }
      

    注意事项

    • panic可以在任何位置引发,但是 recover 只能在 defer 中调用
    • panic("message") 指定 panic 发生时显示的信息

    Part 12 - 数组

    什么是数组

    • 数组是一种数据类型,属于值类型
    • 数组可以存放多个同一类型数据

    数组定义

    var 数组名 [数组大小]数据类型
    var a [5]int
    

    数组初始化的4种方式

    var numArray01 [3]int = [3]int{1,2,3}
    var numArray02 = [3]int{1,2,3}
    var numArray03 = [...]int{1,2,3}  // 长度自行推导
    var numArray04 = [3]{0:1,2:3} // 指定下标
    

    数组遍历

    // 方法1:
    for i := 0; i < len(arr); i++ {
        fmt.Println(arr[i])
    }
    
    // 方法2:
    for index, value := range arr {
        fmt.Println(index, " ", value)
    }
    

    数组对比

    • 数组可以用 == 和 != 进行比较
    • 但不能用 > < >= <= 等符号

    数组相互赋值

    • 当数据类型和数组长度相同时,两个数组之间可以相互赋值

    数组注意事项

    • 数组一旦定义后,长度不能改变,且数组中每个元素都有数据类型对应的默认值

    • 可以通过数组名来获取数组第一个元素的地址,即 数组名 == &数组名[0] == 数组首地址

    • 数组中各个元素的地址间隔由数组类型决定,比如 int64 -> 相隔8,int32 -> 相隔4

    • 数组是值类型,作为参数时,是值传递,即在函数内修改数组,不会对原数组影响,除非使用指针

    • 不能将 [3]int 类型的数组传递给 [4]int 的参数,它们被认为是不同类型;但是 [...]int{1,2,3} 可以传递给 [3]int 的参数

    • 对于指向数组的指针,仍然可以用[index]进行索值

      var arr [5]int{1,2,3,4,5}
      var p = &arr
      // p[3] == arr[3]
      

    二维数组

    • 声明

      var 数组名 [大小][大小]类型
      
    • 赋值

      数组名[n][m] = 123
      
      没有赋值或初始化就是默认值
      
    • 初始化

      var 数组名 [大小][大小]类型 = [][大小][大小]类型{{初值},{初值},{初值}}
      var 数组名 [大小][大小]类型 = {{初值},{初值},{初值}}
      var 数组名 = [大小][大小]类型{{初值},{初值},{初值}}
      var 数组名 = [...][大小]类型{{初值},{初值},{初值}}
      
    • 使用

      fmr.Println(数组名[n][m]) 
      
    • 遍历

      // 方法1:
      for i := 0; i< len(arr); i++ {
          for j := 0; j < len(arr[i]); j++ {
              fmt.Println(arr[i][j])
          }
      }
      
      // 方法2:
      for i,v := range arr {
          for j,v2 := range v {
              fmt.Println(v2)
          }
      }
      
    • 示意图

    6.png

    Part 13 - 切片

    什么是切片

    • 切片是引用类型,指向一个结构体,结构体包括一个数组的指针,切片大小,切片容量
    • 切片的长度是可变的,因为切片底层是动态数组,所以切片的操作和数组很类似

    定义切片

    var 切片名 []类型
    var s []int
    

    切片初始化

    // 如果没有给切片赋值,则是类型的默认值
    // 如果切片没有初始化,也是可以使用的,这与 map 必须初始化后才能使用不同
    
    // 方式1:直接初始化
    var s []int = []int{1,3,5}
    
    // 方式2:由已存在数组创建
    var intArr [5]int = [...]int{1,2,3,4,5}
    s := intArr[1:3] // [1,3)
    // arr[0:end] 等价 arr[:end]
    // arr[start:len(str)] 等价 arr[start:]
    // arr[0:len(str)] 等价 arr[:]
    
    // 方式3:通过 make 来创建切片
    // 通过 make 方法创建的切片,其底层数组是 make 内部维护的,外部不可见
    // 所以切片的值是默认值
    var s []int = make([]int, 4) // 只指定了 length,则capacity == length
    var s []int = make([]int, 4, 10) // []type, length, capacity 
    

    切片遍历

    var arr [5]int = [...]int{10,20,30,40,50}
    slice := arr[0:3]
    
    // 方式1
    for i := 0; i < len(slice); i++ {
        fmt.Println(slice[i])
    }
    
    // 方式2
    for i, v := range slice {
        fmt.Println(i, v)
    }
    

    切片追加元素

    var slice []int = []int{1,2,3}
    slice = append(slice,5,6,7) // 返回新的 slice 
    slice = append(slice,slice) // 可以把slice追加给slice
    

    切片append操作原理

    • 切片append操作本质就是对数组进行扩容
    • Go底层会创建一个新的数组newArr
    • 将slice原来的元素拷贝到新的数组newArr中
    • newArr是底层维护的,程序员不可见
    • 然后创建一个新的sliceNew,sliceNew的ptr指向newArr
    • 最后返回 sliceNew
    • 当切片容量少于等于1000时,以2倍扩容,当大于1000时,以1.25倍扩容

    切片拷贝操作

    copy(para1,para2) //para1 和 para2 都是切片类型,将para2的内容复制到para1
    

    切片内存布局

    • 切片是引用类型,切片名变量存储的是一个结构体的地址,即切片名是结构体的指针

    • 结构体包含三个值:

      • 封装数组的地址

      • 切片的大小

      • 切片的容量

        type slice struct {
            ptr *[2]int
            length int
            capacity int
        }
        

    切片和字符串

    • 字符串底层是 []byte,因此也可以切片

      str := "hello world"
      slice := str[0:5]
      
    2.png
    • 字符串是不可变的

      str[0] = 'z' // error
      
      // 正确
      var temp []byte = []byte(str) // 只能处理字母和数字,因为中文是3个字节
      temp[0] = 'z'               
      str = string(temp)
      
      // 如果想要处理中文,则转为rune即可
      var temp []rune = []rune(str)
      temp[0] = 'z'               
      str = string(temp)
      

    基于原有切片定义新切片

    slice := []int{1, 2, 3, 4, 5}
    slice1 := slice[:]
    slice2 := slice[0:]
    slice3 := slice[:5]
    
    • 设置切片长度和容量一样的好处
    让新切片的长度和容量一样,这样我们在追加操作的时候就会生成新的底层数组,和原有数组分离,就不会因为共用底层数组而引起奇怪问题,因为共用数组的时候修改内容,会影响多个切片
    

    空切片和nil切片

    nil切片: var slice []int
    空切片:  slice := make([]int,0)
    

    Part 14 - 结构体

    定义结构体

    type Cat struct {
        Name string
        Age int
        Color string
        Hobby []string
    }
    

    结构体实例化的4种方法

    // 方法1
    var catInstance Cat // 该变量中的字段的值都是默认类型
    
    // 方法2
    var catInstance Cat = Cat{"小白", 2, "white", []string{"吃鱼","喝奶"}}
    var catInstance Cat = Cat {
        Name: "小白",
        Age: 2,
        Color: "white",
        Hobby: []string{"吃鱼","喝奶"},
    }
    
    // 方法3
    var catInstancePtr *Cat = new(Cat)
    (*catInstancePtr).Name = "小白" //等价于 catInstancePtr.Name = "小白"
    
    // 方法4
    var catInstancePtr *Cat = &Cat{}
    (*catInstancePtr).Name = "小白" //等价于 catInstancePtr.Name = "小白"
    // 不能写 *catInstancePtr.Name = "小白" 因为 . 的优先级高于 *
    

    结构体实例初始化问题

    // 如果没有给字段赋值,则默认为零值
    // 引用类型是nil,即还没有分配空间,对于这样的字段,需要先make,才能使用
    
    var cat1 Cat 
                 
    fmt.Println(cat1.Name, cat1.Age, cat1.Color) //ok
    fmt.Println(cat1.Hobby) //error,需要初始化后才能使用
    

    结构体与tag

    • 在定义结构体时,可以给每个字段加上一个tag

      type Student struct {
          Name string "tag-name"
          Age int "tag-age"
      }
      
    • 结构体的tag可以通过反射机制获取,常见用于序列化和反序列化

    匿名结构

    name := struct {
        字段名1 类型
        字段名2 类型
        字段名3 类型
    } {
        字段名1:value,
        字段名2:value,
        字段名3:value
    }
    //注意匿名的东西都只能使用":="的形式,因为无法指定类型
    

    匿名字段

    type Person struct {
        string
        int
    }
    a := Person{"narlinen",20}
    

    工厂模式

    • 结构体没有构造函数,通过工厂模式来解决

      type student struct {
          Name string
          Age int
      }
      
      func New(name string, age int) *student {
          return &student {
              Name: name,
              Age: age,
          }
      }
      

    结构体注意事项

    • 结构体的所有字段在内存中是连续的
    • 结构体没有构造函数,通过工厂模式创建实例
    • 结构体是用户单独定义的类型,和其他类型进行转换时,需要具有完全相同的字段(名字以及数量,以及类型)
    • 用type给已定义的结构体起别名时,编译器会认为这是新的数据类型,它们之间的赋值需要显式转换
    • 就算字段内容完全相同,但只要struct名字不一样,则就不相等
      • 即 type Duration int64 后,Duration和int64仍然是不同类型,需要强制转换

    Part 15 - 方法

    什么是方法

    • 方法类似于函数,只不过方法可以进行绑定,方法可以绑定到指定的数据类型上,使该数据类型的实例都可以使用这个方法
    • 方法不能独自调用,只能通过绑定的数据类型的实例进行调用
    • 自定义类型都可以有方法,不仅仅是struct

    方法的定义与使用

    type A struct {
        Name string
    }
    
    func (a A) test() {
        fmt.Println(a.Name)
    }
    
    func main() {
        a := A{Name:"111"}
        a.test()
    }
    

    可以添加方法的类型

    • 任何的自定义类型都可以添加相应的方法

    方法注意事项

    • 接收者必须有一个显式的名字,这个名字必须在方法中被使用

    • 如果方法不需要使用接收者的值,可以用 _ 替换它

      func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }
      
    • 类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 int、float 或类似这些的类型上定义方法

    • Go中的toString()方法

      func (a type) String() string { ... }
      

    值接收者和指针接收这

    • 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
    • 对于方法,接收者为值类型时,该类型的值类型或指针类型都可以直接调用,反之亦然
      • 接收者为值类型
        • 调用者为对应值类型:直接调用
        • 调用者为对应类型的指针类型:编译器底层自动解指针后再调用
      • 接收者为指针类型
        • 调用者为对应值类型:编译器底层自动取地址后再调用
        • 调用者为对应值类型的指针类型:直接调用
      • 无论调用者是值类型还是指针类型,实际调用类型还是方法定义的接收者类型

    Part 16 - 封装

    • Go中字段访问权限分为两种:私有字段和公有字段
    • 这两种权限仅仅针对包级别有用
    • 公有字段:首字母大写
    • 私有字段:首字母小写

    Part 17 - 继承

    继承的实现方法

    如果一个 struct 嵌套了另外一个匿名结构体,那么这个结构体就可以直接访问匿名结构体的字段和方法,从而实现继承
    

    继承实现的例子

    type Goods struct {
        Name String
        Price float64
    }
    
    type Book struct {
        Goods // 这里就是嵌套匿名结构体 Goods
        Writer string
    }
    

    继承注意事项

    • 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或小写的字段,方法都可以使用

      type A struct {
          Name string
          age int
      }
      
      func (a *A) SayOK() {
          fmt.Println("A SayOk", a.Name)
      }
      
      func (a *A) hello() {
          fmt.Println("A Hello", a.Name)
      }
      
      type B struct {
          A
      }
      
      func main() {
          
          var b B;
          
          b.A.Name = "tom"
          b.A.age = 10
          b.A.SayOk()
          b.A.hello()
      }
      
    • 匿名结构体字段访问可以简化

      type A struct {
          Name string
          age int
      }
      
      func (a *A) SayOK() {
          fmt.Println("A SayOk", a.Name)
      }
      
      func (a *A) hello() {
          fmt.Println("A Hello", a.Name)
      }
      
      type B struct {
          A
      }
      
      func main() {
          
          var b B;
          
          b.Name = "tom"  
          b.age = 10
          b.SayOk()
          b.hello()
      }
      
    • 当结构体和匿名结构体有相同名字的字段或者方法时

      • 编译器采用就近访问原则,即默认访问该结构体中的字段/方法
      • 编译器会先看结构体中有没有这个字段/方法
        • 如果有,则直接使用这个结构体中的字段/方法
        • 如果没有,则看嵌套的匿名结构体中有没有这个字段/方法
          • 如果有,使用匿名结构体中的字段/方法
          • 如果没有,报错
      • 如果希望访问匿名结构中的字段或方法,可以通过匿名结构体名字显式调用
      type A struct {
          Name string
      }
      
      type B string {
          A
          Name String
      }
      
      func main() {
          var b B = B{
              Name: "I'm B"
          }
          
          b.A.Name = "I'm A"
          
          fmt.Println(b.Name) // 显示 I'm B
          fmt.Println(b.A.Name) // 如果想要使用A的Name,则需要显式说明
      }
      
    • 结构体中嵌入两个或以上的匿名结构体,如果两个匿名结构体具有相同名字的字段/方法(同时结构体本身没有这个名字的字段/方法),则在访问时,必须显式指定匿名结构体的名字,否则编译器报错

      type A struct {
          Name string
          age int
      }
      
      type B struct {
          Name string
          Score float64
      }
      
      type C struct {
          A
          B
      }
      
      func main() {
          var c C
          c.Name = "tom" // error
          c.A.Name = "A-tom" // ok
          c.B.Name = "B-tom" // ok
      }
      
      type A struct {
          Name string
          age int
      }
      
      type B struct {
          Name string
          Score float64
      }
      
      type C struct {
          A
          B
          Name string
      }
      
      func main() {
          var c C
          c.Name = "tom" // ok,因为结构体本身具有 Name
      }
      
    • 嵌套基本类型

      type A struct {
          Name string
          age int
      }
      
      type Stu struct {
          A
          int
      }
      
      func main() {
          stu := Stu{}
          
          stu.Name = "tom"
          stu.age = 100
          stu.int = 666
          
          fmt.Println(stu)
      }
      

    继承的初始化

    type Brand struct {
        Name string
        Address string
    }
    
    type Goods struct {
        Name string
        Price float64
    }
    
    type TV struct {
        Goods
        Brand
    }
    
    func main() {
        tv := TV {
            Goods{
                Name: "电视机",
                Price: 15.5
            },
            Brand{"海尔","上东"},
        }
    }
    

    使用指针形式的继承

    type Brand struct {
        Name string
        Address string
    }
    
    type Goods struct {
        Name string
        Price float64
    }
    
    type TV struct {
        *Goods
        *Brand
    }
    
    func main() {
        tv := TV {
            &Goods{
                Name: "电视机",
                Price: 15.5
            },
            &Brand{"海尔","上东"},
        }
    }
    
    

    Part 18 - 组合

    组合与继承的关系

    • 组合和继承基本相同

    • 区别在于在结构体中嵌套其他结构体时,进行命名,而不是匿名

      type Person struct {
          Name string
          Age int
      }
      
      type Student struct {
          person Person
          Score float64
      }
      
    • 对于组合,如果想要使用嵌套结构体中的字段/方法,则必须显式说明嵌套结构体的名字

      type Person struct {
          Name string
          Age int
      }
      
      type Student struct {
          person Person
          Score float64
      }
      
      func main() {
          var stu Student =Student{}
          
          stu.person.Name = "gothicrush"
          stu.person.Age = 21
          
          stu.Score = 100.0
      }
      

    Part 19 - 接口

    接口定义

    type usb interface {
        Start()
        Stop()
    }
    

    接口实现

    • Go中不需要显式声明继承、实现了接口
    • 只要一个类型实现了接口中所有的方法,则编译器就认为该类型实现了接口
    type Phone struct {
        
    }
    
    func (p Phone) Start() {
        fmt.Println("手机连接成功")
    }
    
    func (p Phone) Stop() {
        fmt.Println("手机停止连接")
    }
    

    接口注意事项

    • 接口中声明一组方法,不需要也不能实现

    • 接口中不能有任何变量或常量,只能有声明的方法

    • 接口类型是引用类型,如果没有对它赋值,则默认为nil

    • 接口本身不能拥有实例,但是接口类型的变量可以指向一个实现了该接口的数据类型变量(即多态)

    • 只要是自定义数据类型,都可以实现接口,不仅仅是结构体

      type Speak interface {
          Say()
      }
      
      type integer int
      
      func (i integer) Say() {
          fmt.Println("I'm integer")
      }
      
    • 一个自定义类型可以实现多个接口

    • 一个接口A可以继承一个或多个其他接口,此时,如果想要实现接口A,则需要把A继承的接口也都全部实现

      • 如果多个接口中有方法的名字相同,则会报错
        • 就算参数不同,返回值不同也不行,只要名字相同就报错,因为Go不支持函数重载
      type BInterface interface {
          testB()
      }
      
      type CInterface interface {
          testC()
      }
      
      type AInterface interface {
          BInterface
          CInterface
          testA()
      }
      
      type Student struct {
          
      }
      
      func (stu *Student) testB() {
          
      }
      
      func (stu *Student) testC() {
          
      }
      
      func (stu *Student) testA() {
          
      }
      
    • 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口

      interface{} 这个类型的变量可以指向任何类型的变量
      所以 interface{} 类似 Java 的 Obeject
      
    • 当实现方法时,struct 和 *struct 是不同的

      type Usb interface {
          Say()
      }
      
      type Student struct {
          
      }
      
      func (stu *Student) Say() {
          fmt.Println("Say..")
      }
      
      func main() {
          var stu Student = Student{}
          var u1 Usb = stu  // 报错,因为是 *Student 实现了Usb,而不是 Student
          var u2 Usb = &stu // ok
      }
      

    接口和继承的关系

    • 当结构需要扩展功能,同时不希望去改变或破坏继承关系,则可以去实现某个接口
    • 可以认为接口是对继承机制的补充
    • 继承:解决代码的复用性和可维护性
    • 接口:用于设计和规范代码

    Part 20 - 多态

    • Go的多态依赖接口实现,因为 Go 中没有像 Java 那样的继承

    Part 21 - 类型断言

    什么是类型断言

    • 由于多态的存在,接口变量不知道其指向的具体类型,如果需要转为具体类型,则需要使用类型断言

    类型断言语法

    接口变量名.(具体类型) // 此处变量必须为 interface 类型
    

    类型断言返回值

    x := 变量名.(具体类型) // 如果转换成功则返回给x,转换失败则抛出 panic
    x,ok := 变量名.(具体类型) // 如果转换成功则返回给x,ok为true,转换失败则 ok 为 false,x为类型默认值
    

    类型断言例子

    type Point struct {
        x int
        y int
    }
    
    func main() {
        var a interface{}
        var point = Point{1,2}
        
        a = point
        
        var b Point
        
        // b = a //不行,虽然 a 指向的是Point类型,但是现在 a 是 Point 类型
        b = a.(Point) // 可以,这就是类型断言,表示判断 a 是否指向 Point 类型的变量
                      // 如果是则转为 Point 类型并赋值给 b 变量,否则抛出 panic
    }
    

    类型断言的最佳实践

    func checkType(items ...interface{}) {
        
        for index, x := range itmes {
            
            switch x.(type) { // 这里 type 是关键字,固定写法,只能用于 switch 语句
            case bool:
                fmt.Println("bool")
            case float64:
                fmt.Println("float64")
            case string:
                fmt.Println("string")
            case Student:
                fmt.Println("Student")
            case *Student:
                fmt.Println("*Student")
            }
        }
    }
    

    Part 22 - 单元测试

    传统测试方法

    • 在 main 函数中,调用需要测试的函数,看看实际结果与预期是否相同,如果相同,则正确,否则不正确
    • 缺点:
      • 不方便,我们需要在 main 函数中调用,如果项目正在运行,则可能需要去停止项目
      • 不利于管理,不管什么模块的方法的测试都写在 main 函数里面了

    单元测试框架

    • Go中自带有一个轻量级测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试

    • 使用流程

      1. 创建测试文件

        以 xxx_test.go 格式命名
        
      2. 导入测试框架

        import "testing"
        
      3. 测试函数的签名

        // 参数类型必须为 *testing.T(单元测试)或 *testing.B(性能测试)
        // 必须以Test开头,Test后的下一个字母必须是大写字母,否则单元测试不会执行这个函数
        func TestXxx(t *testing.T) { 
                                     
        }                            
        
      4. 编写测试函数

        func TestXxx(t *testing.T) {
            t.SkipNow() // 跳过当前测试函数
            t.Fatalf("错误")
            t.Errorf("错误")
            t.Logf("正确")
        }
        
      5. 执行测试

        go test // 所有测试文件都进行测试,如果运行正确,无日志,错误时,有日志
        go test -v // 所有测试文件都进行测试,无论运行是否正确,都有日志
        go test xxx_test.go // 指定测试文件
        go test -v xxx_test.go // 指定测试文件
        go test -v -test.run TestAdd // 只测试指定的函数
        
      6. 保证多个测试函数按顺序执行

        func TestAll(t *testing.T) {      // 不一定是叫 TesttAll
            t.Run("TestPrint",TestPrint)  // TestPrint为函数名
            t.Run("TestPrint2",TestPrint2)
        }
        
      7. TestMain

        • 固定名字
          • func (m testing.M) {}
        • 测试函数最开始执行这个函数,一般用于测试环境初始化
        • 如果 TestMain 中不写 m.Run(),则其他测试函数不会执行

    benchmark

    • 用于性能测试

    • 参数为(b *testing.B),程序会执行 b.N 次,在执行过程中,会根据实际case的执行时间是否稳定改变b.N的值,以达到稳态

    • 例子

      func BenchmarkTest(b *testing.B) {
          for n := 0; n < b.N; n++ {
              function()
          }
      }
      
    • benchmark中的函数必须是稳定的,否则测试程序不能停止

      func increse(n int) int {
          for n > 0 {
              n--
          }
          return n
      }
      
      func BenchmarkAll(b *testing.B) {
          for n := 0; n < b.N; n++ {
              increase(n) // 函数每次执行时间都不同
          }
      }
      

    Part 23 - JSON/序列化/反序列化

    JSON概念以及作用

    • JSON全程 JavaScript Object Notation,是一种轻量级的数据交换格式,易于阅读和书写,也方便机器进行生成和解析
    • 在数据传输前,传输的数据会经过处理变为 JSON 形式的字符串后再通过网络传输,接收方接收到 JSON 字符串后会进行处理,将 JSON字符串转为原始的数据
    • 序列化:原始数据 -> JSON字符串
    • 反序列化:JSON字符串 -> 原始数据

    序列化所需的包以及函数

    import "encoding/json"
    
    func Marshal(v interface{}) ([]byte, error)
    

    结构体进行序列化

    • 将要进行序列化的结构体

      type Monster struct {
          Name string
          Age int
          BirthDay string
          Sal float64
          Skill string
      }
      
    • 结构体实例化实例

      import "encoding/json"
      
      func main() {
          monster := {
              Name: "牛魔王",
              Age: 500,
              Birthday: "2011-11-11",
              Sal: 8000.0,
              Skill: "牛魔拳",
          }
          
          data,err := json.Marshal(monster)
          if err != nil {
              fmt.Println("序列化失败", err)
          } else {
              fmt.Println("序列化后:", string(data))
          }
      }
      
    • 结构体使用tag自定义序列化后 key 值的名称

      // `json:"xxx"`
      // 反引号是必须的
      type Monster struct {
          Name string `json:"name"`
          Age int     `json:"age"`
          BirthDay string `json:"birthday"`
          Sal float64 `json:"sal"`
          Skill string `json:"skill"`
      }
      

    map进行序列化

    • 将要进行序列化的map

      var m map[string]interface{}
      m["name"] = "红孩儿"
      m["age"] = 30
      m["address"] = "洪崖洞"
      
    • map实例化实例

      import "encoding/json"
      
      func main() {
          
          var m map[string]interface{}
          m["name"] = "红孩儿"
          m["age"] = 30
          m["address"] = "洪崖洞"
          
          data,err := json.Marshal(m)
          if err != nil {
              fmt.Println("序列化失败", err)
          } else {
              fmt.Println("序列化后:", string(data))
          }
      }
      

    切片进行序列化

    • 将要进行序列化的切片

      var slice []int = []int{1,2,3,4,5}
      
    • 切片实例化实例

      import "encoding/json"
      
      func main() {
          
          var slice []int = []int{1,2,3,4,5}
          
          data,err := json.Marshal(slice)
          if err != nil {
              fmt.Println("序列化失败", err)
          } else {
              fmt.Println("序列化后:", string(data))
          }
      }
      

    序列化所需的包以及函数

    import "encoding/json"
    
    func Unmarshal(s []byte, v interface{}) (err error) 
    

    反序列化为结构体

    package main
    
    import "encoding/json"
    import "fmt"
    
    func main() {
    
        str := `{"address":"洪崖洞","age":30,"name":"红孩儿"}`
    
        var monster Monster
    
        err := json.Unmarshal([]byte(str), &m)
    
        if err != nil {
            fmt.Println("序列化失败", err)
        } else {
            fmt.Println("序列化后:", m)
        }
    }
    
    

    反序列化为map

    package main
    
    import "encoding/json"
    import "fmt"
    
    func main() {
    
        str := `{"address":"洪崖洞","age":30,"name":"红孩儿"}`
    
        var m map[string]interface{}
        m = make(map[string]interface{})
        m["one"] = 1
    
        err := json.Unmarshal([]byte(str), &m)
    
        if err != nil {
            fmt.Println("序列化失败", err)
        } else {
            fmt.Println("序列化后:", m)
        }
    }
    
    

    反序列化为切片

    package main
    
    import "encoding/json"
    import "fmt"
    
    func main() {
    
        str := `{"address":"洪崖洞","age":30,"name":"红孩儿"}`
    
        var m map[string]interface{}
    
        err := json.Unmarshal([]byte(str), &m)
    
        if err != nil {
            fmt.Println("序列化失败", err)
        } else {
            fmt.Println("序列化后:", m)
        }
    }
    
    

    注意事项

    • 结构体在序列化时,非导出变量(小写字母开头)不会被 encode,因此在 decode 的时候这些非导出变量的值为其类型的零值

      Part 24 - 反射

    反射的概念与作用

    • 反射可以在运行时动态获取变量的各种信息,比如变量的类型
    • 如果变量是结构体,还可以获取结构体本身的信息,比如结构体的字段,方法
    • 通过反射,可以修改变量的值,可以调用变量关联的方法

    反射的使用

    • 反射包

      import "reflect"
      

    reflect.Value

    • 由 reflect.ValueOf(v interface{}) (t reflect.Value) 获取某个变量的 Value

    • reflect.Value.Kind:获取变量的类别,返回的是一个常量

    • 变量,interface{},reflect.Value 之间可以任意转换

      // interface{} --> reflect.Value
      rVal := reflect.ValueOf(v)
      
      // reflect.Value --> interface{}
      iVal := rVal.Interface()
      
      // interface{} --> 变量 
      variable := iVal.(int64)
      
      // 变量 --> interface{}
      iVal = variable
      
    3.png

    reflect.Type

    • 由 reflect.TypeOf(v interface{}) (t reflect.Type) 获取某个变量的 Type
    • reflect.Type.Kind:获取变量的类别,返回的是一个常量,与 reflect.Value.Kind 相同
    • Type是类型,Kind是类别,它们可能相同,可能不同
      • var num int = 10 Type:int Kind:int
      • var stu Student Type:包名.Student Kind:struct

    Part 25 - 常量

    • 常量用 const 修饰(const替代var)

    • 常量在定义时必须初始化,一旦初始化就不能更改

    • const只能修饰 bool类型,数值类型,string类型

    • 常量中如果想要使用函数,则只能使用内置函数

    • 常量不能用 := 创建,因为需要 const

    • 常量简洁写法

      const (
          a = 1
          b = 2
      )
      
    • 常量借助iota实现枚举

      const (      // iota是一个内置的变量,用于存储 const 组中常量的个数
          a = iota // 0,iota从零开始,组内每定义一个常量,自增1
          b        // 1
          c        // 2
      )
      // 每遇到一个const,iota自动清零
      const (
          d = iota // 0
          e, f = iota, iota // 1, 1
          g = iota // 2
      )
      
    • Go没有常量必须大写字母开头的规范,因为常量仍然有大小写字母开头控制访问范围的机制

    Part 26 - 网络编程

    网络编程分类

    • 基于 TCP/IP 的 Socket编程
    • 基于 HTTP 的 HTTP 编程

    端口

    • 0是保留端口
    • 1-1024是知名端口
      • 21:ftp
      • 22:ssh
      • 23:telnet
      • 24:smtp
      • 80:http
    • 1025-65535是动态端口

    Socket 的使用流程

    • 服务端

      • 监听端口
      • 接收客户端发送的 tcp 连接,建立与客户端的 tcp 连接
      • 创建 goroutine,处理连接请求
      • 关闭连接
    • 客户端

      • 建立与服务端的连接
      • 发送请求
      • 接收服务器端返回的处理数据
      • 关闭连接
    • 示意图

    4.png

    Socket 实例例子

    • server.go

      package main
      
      import (
          "fmt"
          "net"
      )
      
      func process(conn net.Conn) {
          
          defer conn.Close()
          
          for {
              buf := make([]byte,1024)
              n, err := conn.Read(buf)
              if err != nil {
                  fmt.Println("服务器的err=",err)
                  return
              }
              fmt.Println(string(buf[:n])"客户端发送了")
          }
          
      }
      
      func main() {
          
          fmt.Println("服务器开始监听")
          // 1. tcp : 表示使用的协议是tcp
          // 2. 0.0.0.0:8888 :表示监听的端口是8888
          listen, err := net.Listen("tcp", "0.0.0.0:8888")
          
          if err != nil {
              fmt.Println("err=", err)
              return
          }
          
          // 延时关闭资源
          defer listen.Close()
          
          for {
              // 等待客户端连接
              conn, err := listen.Accept()
              if err != nil {
                  fmt.Println("err=", err)
                  return
              } else {
                  fmt.Printf("连接成功,客户端ip=%v",conn.RemoteAddr().String())
              }
              
              go process(conn)
          }
      }
      
    • client.go

      package main
      
      import (
          "fmt"
          "net"
      )
      
      func main() {
          
          conn,err := net.Dial("tcp","192.168.20.253:8888")
          
          if err != nil {
              fmt.Println("err=", err)
          }
          
          defer conn.Close()
          
          reader := bufio.NewReader(os.Stdin)
          line, err := reader.ReadString("\n")
          
          if err != nil {
              fmt.Println("err=", err)
          }
          
          n, err := conn.Write([]byte(line))
          
          if err != nil {
              fmt.Println("err=", err)
          }
          
          fmt.Printf("客户端发送了 %d 个字节,并退出", n)
      }
      

    Part 27 - 映射

    映射类型声明的三种方式

    • 仅仅进行声明

      var 变量名 map[keyType]valueType
      
    • 声明时直接初始化

      var 变量名 map[string]int = make(map[string]int, 10) // 10可以省略
      var 变量名 map[string]int = make(map[string]int) // 10可以省略
      
    • 声明时直接赋值

      var 变量名 map[string]int = map[string]int{
          "one": 1,
          "two": 2,
      }
      

    映射 key 和 value 的要求

    • key 必须支持 == 运算
      • 一般用 string 和 数值 ,还可以用 bool,指针,channel
      • 不能用 slice,map和function
    • value可以是string,数值,bool,struct,map 不能用slice,map和function

    映射的赋值 [增,改]

    • m["key"] = value
    • 当m中本身没有 key 时,会新增
    • 当m中本身有 key 时,会覆盖
    • 当空间不够时,会自动扩容

    映射的删除 [删]

    • delete(m, "one")
    • 当删除的 key 不存在时,既不操作也不报错
    • Go中没有方法可以一次性清除整个map,如果想,则可以遍历,或者让变量指向一个新的 map,让 gc 把原来那个删除了

    映射的查找 [查]

    • val := m["one"]
    • 访问不存在的 key 值,返回类型默认类型,而不报错
    • val, findRes := m["one"]
    • 如果找到了 val 为 key 对应值,findRes为 false
    • 如果找不到 val 为 类型默认值 ,findRes为true

    映射的遍历

    • 不能用 for 循环,因为映射是无序的

    • 需要使用 for-range 循环,其中 k 和 v 是拷贝的

      for k,v := range m {
          fmt.Printf("k=%v,v=%v",k,v)
      }
      
    • golang中的map是无序的,每次遍历的结果都可能不一样

    映射的排序

    • golang中的map是无序的,每次遍历的结果都可能不一样

    • golang中没有专门的方法针对map的key进行排序

    • 如果想要对映射排序,则可以先拿出所有的key,将key进行排序,再取出value

      var keys []int
      
      for key, _ := range m {
          keys = append(keys, key)
      }
      
      sort.Ints(key)
      

    映射注意事项

    • 声明是不会分配内存的,默认值为nil
    • 需要使用make来初始化,分配内存后才能使用,make就是给map分配空间
    • map的存储是无序的
    • 使用内建函数 len 可以获取映射中键值对的个数
    • 映射底层的数据结构是哈希表,所以无序

    Part 28 - goroutine(协程)

    Go的进程和Go的协程

    • Go进程:
      • 一个Go进程上,可以启动多个协程
    • Go协程:
      • 有独立栈空间
      • 共享堆空间
      • 调度由用户定义
      • 协程是轻量级线程
    • Go协程对比其他语言并发实现的优势
      • Go协程是进程开启的,是轻量级的线程,对资源耗费小
      • 其他语言的并发一般是基于线程的
      • Go协程可以轻松开上万个,而其他语言开上万个并发较为吃力

    Go协程的使用

    // 编写一个程序,满足以下功能:
    // 1. 在进程中,开启一个 goroutine,该协程每隔1秒输出 "hello world"
    // 2. 在进程中,也每隔1秒输出 "hello golang",输出10次后,进程结束
    // 3. 要求进程和 goroutine 同时执行
    
    package main
    
    import (
        "fmt"
        "time"
        "strconv"
    )
    
    func test() {
        for i := 0; i < 10; i++ {
            fmt.Println("[test] hello world" + strconv.Itoa(i))
            time.Sleep(time.Second)
        }
    }
    
    func main() {
        
        go test() // 开启一个协程,执行这个 test
        
        for i := 0; i < 10; i++ {
            fmt.Println("[main] hello golang" + strconv.Itoa(i))
            time.Sleep(time.Second)
        }
    }
    

    goroutine协程的调度模型

    • MPG模型
    • M:操作系统上的进程(主线程)
    • P:协程需要的上下文
    • G:协程

    查看和设置Go运行的cpu数目

    • 概述

      • go1.8以后,默认让程序运行多个核,可以不设置
      • go1.8之前,最好进行设置,从而提高性能
    • 查看系统的逻辑cpu数目

      import "runtime"
      
      fmt.Println(runtime.NumCPU())
      
    • 设置执行Go程序的cpu个数

      import "runtime"
      
      runtime.GOMAXPROCS(num) // 本函数在编译优化时会被去掉
      

    goroutine中使用 recover

    • goroutine中使用 recover,可以解决某个协程中出现 panic,导致整个程序崩溃的问题

      defer func() {
          if err := recover(); err != nil {
              fmt.Println("发生错误", err)
          }
      }
      

    Part 29 - channel

    goroutine资源竞争的问题

    // 下面程序会发成 资源竞争问题(concurrent map writes)
    // 查看是否发生资源竞争问题可以用命令:go build -race test.go
    // 现采用goroutine计算1-200各个数的阶乘,并把每个数的阶乘放进map中,最后显示出来
    
    package main
    
    import (
        "fmt"
        "time"
    )
    
    var (
        myMap = make(map[int]int, 10)
    )
    
    func calculate(n int) {
        
        res := 1
        for i := 1; i <= n; i++ {
            res *= i
        }
        
        myMap[n] = res
    }
    
    func main() {
        for i := 1; i <= 200; i++ {
            go test(i)
        }
        
        time.Sleep(20 * time.Second) // 避免goroutine还没执行,主线程就结束了
        
        for k,v := range myMap {
            fmt.Printf("map[%d]%d\n", k, v)
        }
    }
    

    解决资源竞争问题 - 通过全局变量加锁

    package main
    
    import (
        "fmt"
        "time"
        "sync"
    )
    
    var (
        myMap = make(map[int]int, 10)
        lock sync.Mutex // 全局的互斥锁
    )
    
    func calculate(n int) {
        
        res := 1
        for i := 1; i <= n; i++ {
            res *= i
        }
        
        lock.Lock()    // 加锁
        myMap[n] = res
        lock.Unlock()  // 解锁
    }
    
    func main() {
        for i := 1; i <= 200; i++ {
            go calculate(i)
        }
        
        time.Sleep(20 * time.Second) // 避免goroutine还没执行,主线程就结束了
        
        lock.Lock()    // 加锁
        for k,v := range myMap {
            fmt.Printf("map[%d]%d\n", k, v)
        }
        lock.Unlock()  // 解锁
    }
    

    解决资源竞争问题 - 通过channel

    全局变量锁的缺点

    • 主线程等待所有 goroutine 全部完成的时间很难确定
    • 通过全局变量锁实现的同步,不利于多个协程对全局变量的读写操作

    channel 介绍

    • channel 是一个引用类型
    • channel的本质是一个队列
    • 数据是先入先出的,即多个 goroutine 同时访问时,也不需要加锁
    • channel是有类型的,一个string类型的channel只能存放string

    channel 声明

    var 变量名 chan 数据类型
    
    var intChan chan int
    var mapChan chan map[int]string
    var perChan chan Person
    

    channel 初始化

    • channel 必须初始化后才能使用,即 make 后
    var intChan chan int = make(chan int, 3)
    

    channel 写入数据

    intChan <- 10
    
    // channel数据放满后就不能再放了,容量不是动态增长的
    

    channel读取数据

    var num int
    num = <- intChan
    
    // 在没有使用协程的情况下,当channel已经为空,但是依然继续取时,会报 deadlock
    

    channel长度和容量

    len(intChan)
    cap(intChan)
    

    channel关闭

    // 关闭 channel 后,就不能再往里面写数据了,但如果 channel 里面还有数据,则可以继续读取
    close(intChan)
    

    channel遍历

    // 在遍历时,如果 channel 还没关闭,则会报 deadlock
    // 在遍历时,如果 channel 已经关闭,则遍历正常执行
    
    for v := range intChan {
        fmt.Println("v=", v)
    }
    

    channel阻塞机制

    如果只向 channel 中写入数据而不读取,导致 channel 满了,还继续写,就会出现阻塞而 deadlock
    
    如果读的频率远低于写的频率也没有问题,也不会发生 deadlock,关键是要去读
    

    channel 只读与只写

    var chan1 chan int // 可读可写
    var chan2 chan <- int // 只写
    var chan3 <- chan int // 只读
    

    channel与select

    // 使用 select 可以解决从管道取数据阻塞的问题
    
    // 1. 定义一个管道 10个数据int
    intChan := make(chan int, 10)
    for i := 0; i < 10; i++ {
        intChan <- i
    }
    // 2. 定义一个管道  5个数据string
    strChan := make(chan string, 5)
    for i := 0; i < 10; i++ {
        intChan <- "hello" + fmt.Sprintf("%d", i)
    }
    
    // 在 for-range 遍历管道时,如果不关闭管道,会发生 deadlock 现象
    // 但是我们可能不好确定什么时候该关闭管道
    // 可以使用 select 方法解决
    for {
        select {
            // 这里,如果管道没有关闭,也不会因为一直阻塞而 deadlock
            // 会自动到下一个 case 匹配
            case v := <- intChan:
                fmt.Println("从 intChan 读取数据%v\n", v)
            case v :=  <- strChan:
                fmt.Println("从 strChan 读取数据%v\n", v)
            default:
                fmt.Println("都没取到")
        }
    }
    

    通过 channel 解决素数问题

    思路图示

    5.png

    代码实现

    package main
    import (
        "fmt"
    )
    
    func putNum(intChan chan int) {
        
        for i := 0; i < 1000; i++ {
            intChan <- i
        }
        
        close(intChan)
    }
    
    func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
        var flag bool
        for {
            num, ok := <- intChan
            
            if !ok {
                break
            }
            
            flag = true
            for i := 2; i < num; i++ {
                if num % i == 0 {
                    flag = false
                    break
                }
            }
            
            if flag {
                primeChan <- num
            }
        }
        
        fmt.Println("有一个primeChan因为取不到数了而退出")
        exitChan <- true
    }
    
    func main() {
        
        intChan := make(chan int, 1000)
        primeChan := make(chan int, 1000)
        exitChan := make(chan bool, 4)
        
        go putNum(intChan)
        
        for i := 0; i < 4; i++ {
            go primeNum(intChan, primeChan, exitChan)
        }
        
        go func() {
            for i := 0; i < 4; i++ {
                <- exitChan
            }
    
            close(primeNum)
        }()
        
        for {
            res, ok := <- primeNum
            if !ok {
                break
            }
            fmt.Printf("素数:%d\n", res)
        }
        
        fmt.Println("main线程退出")
    }
    

    Part 30 - go命令行工具

    • go 在命令行输入 go,即可查看go命令行工具的说明
    • go build 生成可执行二进制文件
    • go clean 在go build后会残留一些临时文件,可以用go clean清除
    • go run 执行go程序
    • go install 类似go build,区别是生成的可执行二进制文件在指定位置
    • go get 获取网上的 go 包
    • go doc 在线获取包的文档
    • go fmt 格式化代码
    • go vet 检测代码中的语法错误
    • go env 查看当前go环境

    Part 31 - Go操作Redis

    安装 Go 的 redis 插件

    go get github.com/garyburd/redigo/redis
    

    Go 中连接 redis

    connect, err := redis.Dial("tcp","localhost:6379")
    

    Go 中执行 redis 命令

    _, err := connect.Do("Set", "key1", 998)
    
    result, err := connect.Int(c.Do("Get", "key1")) // 要使用类型断言
    

    Go 中使用 redis 连接池

    • 连接池的作用
      • 事先初始化一定数量的连接,放入连接池中
      • 当Go需要操作redis时,直接从redis连接池中取出连接即可
      • 这样可以节省获取redis连接的时间
    • 核心代码
    var pool *redis.Pool
    
    pool = &redis.Pool {
        MaxIdle: 8,     // 最大空闲连接数
        MaxActive: 0,   // 表示和数据库的最大连接数,0表示不限制
        IdleTimeout: 100, // 最大空闲时间,单位秒
        Dial: func() (redis.Conn, error) { // 初始化连接池的代码
            return redis.Dial("tcp", "localhost:6379")
        },
    }
    
    connect := pool.Get() // 从连接池中取出连接
    
    pool.Close() // 关闭连接池
    

    Part Max - 补充内容

    随机数使用

    import "rand"
    
    // 设置随机数种子
    rand.Seed(time.Now().UnixNano())
    
    // 产生随机数
    rand.Intn(100) // 0<=n<100
    

    CGO

    • CGO是Go语言调用C代码的模块,是C语言和Go语言之间的桥梁
    • 原则上无法直接支持 C++ 的类,因为C++至今为止都没有一个二进制接口规范

    VS Code 使用技巧

    反射再看一遍

    网络再看一遍

    相关文章

      网友评论

        本文标题:Go 学习笔记

        本文链接:https://www.haomeiwen.com/subject/yiazwftx.html