Go 学习笔记

gothicrush · · 841 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

[内容][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 使用技巧

反射再看一遍

网络再看一遍


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:gothicrush

查看原文:Go 学习笔记

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

841 次点击  ∙  1 赞  
加入收藏 微博
1 回复  |  直到 2018-09-06 00:09:58
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传