跟着尚硅谷B站视频记的笔记


入门

  • go 编译和运行源代码

    • go build 编译源代码,生成可执行文件

      • go build -o newName.exe name.go
    • go run 直接编译运行代码
  • godoc

  • gofmt 格式化代码


1 遇到的问题

1.1 安装环境问题

换成国内源

go env -w GOPROXY=https://goproxy.cn,direct


1.2 报错

package XXX is not in GOROOT (X:\XXX\Go\src\XXX)

解决方法:go env -w GO111MODULE=off


2 语法

2.1 语法注意事项

  1. go 语言定义的变量或者 import 的包如果没有使用,代码不能编译通过。

2.2 基本语法

2.2.1 声明变量

var 变量名 变量类型

// 通常在函数内使用
s := ""
// 声明后若不赋值,则使用默认值
var s string
// 建议使用前两种形式
var s = ""
var s string = ""

有多个变量需要声明时,可以使用:

var (
n1 = 100
n2 = 200
)

注意事项

  1. 在函数外部定义的变量是全局变量
  2. 变量在同一个作用域内不可重名。
  3. 在对字符串(string类型)的数据进行加法时,直接拼接。

3 数据类型

uint 的 u 表示 无符号。

3.1 基本数据类型

整型

类型 有无符号 占用空间 备注
int 32位系统:4 Bytes
64位系统:8 Bytes
int8 1 Byte 不可以加溢出 int8 范围的整数,即大于会等于 128 的数字
int16 2 Bytes
int32 4 Bytes
int64 8 Bytes
uint int
rune int32 4 Bytes int32 等价,
表示一个 Unicode 码
byte uint8 1 Bytes 储存单个字符时使用
  • 查看某个变量占用的字节数:unsafe.Sizeof(variable)

浮点型

类型 占用空间
float32 (单精度) 4 Bytes
float64 (双精度) 8 Bytes
  • 浮点数都是有符号的。

  • 位数部分可能丢失,造成精度损失。如:

    var num1 float32 = -123.0000901
    var num2 float64 = -123.0000901
    // num1 和 num2 实际的值分别为
    // -123.000090 和 -123.0000901
  • 浮点型默认声明为 float64 类型。

  • 浮点型还可表示为科学计数法形式,使用 Ee,如 num := 1.23E2

没有专门类型的字符型

  • 使用 byte 来储存。

  • Go 的字符串是由字节组成的。

  • 如果保存的字符的对应码值大于 255,可以使用 int 型来储存。


  • 如果需要以安装字符的方式输出,使用格式化输出(如:fmt.Printf("%c", 'a')

  • 允许使用转义字符,如:fmt.Printf("%c好的", '\t')

  • 可以进行运算。

布尔型

truefalse。占用空间为 1 Byte。

字符串型

  • Go 中,字符串是不可变的;所以字符串一旦赋值,就不能修改。
  • 可以使用双引号("")和反引号(``)
    • 双引号:会识别转义字符;
    • 反引号:以字符串的原有形式输出,包括转义字符;可实现防止攻击、输出源代码等功能。
  • 字符串的拼接:str1 + str2
    • 如果需要多行, + 需要保留在上一行。

3.2 各数据类型的默认值

数据类型(基本) 默认值
整型 0
浮点型 0
字符串型 ""
布尔型 false
数据类型(引用) 默认值
数组 和其元素类型有关
指针 nil
slice nil
map nil
  • nil 即还没有分配空间。

3.3 数据类型转换

由于 Go 中的数据类型不能自动转换,所以在不同类型的变量之间赋值时需要显式转换

示例:newType(variable) ,把 变量 variable 转换为 newType 类型。

var i float32 = 10.01
n1 := int(i)
fmt.Println(n1)
// 输出为:10

3.3.1 基本数据类型和 string 型的转换

基本数据 → string

  • [方式1] fmt.Sprint("%参数", 表达式) :Sprint根据format参数生成格式化的字符串并返回该字符串。

    • 参数需与表达式的数据类型相匹配

    • 例:

      var num1 int = 99
      var num2 float64 = 23.456
      var b bool = true
      var myChar byte = 'h'
      var str string str = fmt.Sprintf("%d", num1)
      fmt.Printf("str type %T str=%q\n", str, str) str = fmt.Sprintf("%f", num2)
      fmt.Printf("str type %T str=%q\n", str, str) str = fmt.Sprintf("%t", b)
      fmt.Printf("str type %T str=%q\n", str, str) str = fmt.Sprintf("%c", myChar)
      fmt.Printf("str type %T str=%q\n", str, str)
      // 输出为
      // str type string str="99"
      // str type string str="23.456000"
      // str type string str="true"
      // str type string str="h"
  • [方式2] 使用 strconv 包的函数

    func FormatBool(b bool) string
    func FormatFloat(f float64, fmt byte, prec, bitSize int) string
    // str = strconv.FormatFloat(num, 'f', 10, 64)
    // 表示格式为'f',保留位数为10,小数类型是float64类型。
    func FormatInt(i int64, base int) string
    // str = strconv.FormatInt(num, 10)
    func FormatUint(i uint64, base int) string

string → 基本数据类型

使用 strconv 包的函数

func ParseBool(str string)(value bool, err error)
// 返回两个值(value和err)
func ParseFloat(s string, bitSize int)(f float64, err error)
// 返回的数值是float64型的数值
func ParseInt(s string, base int, bitSize int)(i int64, err error)
// int64
func ParseUint(s string, b int, bitSize int)(n uint64, err error)
// uint64

例:

var str string = "true"
var b bool
b, _ = strconv.ParseBool(str)
// b = true var str2 string = "1234590"
var n1 int64
n1, _ = strconv.ParseInt(str2, 10, 64)
// n1 = 12345 var str3 string = "123.456"
var f1 float64
f1, _ = strconv.ParseFloat(str3, 64)
// f1 = 123.456
  • 如果需要其他类型的,得到结果之后进行显示转换即可。

  • 应确保 string 型的数据能转换成有效的数据。

    在不能转换为有效的数据时,得到的数据是相应数据类型的默认值

    • 如将 "hello" 转为整数时,得到 0

3.4 派生数据类型

指针 Pointer

  • 获取变量的地址:&v

  • 定义指针类型的变量:var ptr *type = &v

  • 获取指针指向变量的值:*ptr


  • 值类型都有对应的指针类型,形式为 *type

    • 值类型包括:int系列、float系列、bool、string、数组和结构体。

3.5 自定义数据类型

  • 语法

    type typeName 数据类型 // 相当于一个别名
    // 如: type myInt int
    // type mySum func(int, int) int

3.6 值类型和引用类型

  • 范围

    • 值类型:int系列、float系列、bool、string、数组和结构体。
    • 引用类型:指针、切片slice、map、管道chan、interface等。
  • 使用特点

    • 值类型:变量直接存储值,内存通常在栈中分配。
    • 引用类型:变量储存的是一个地址,内存通常在堆上分配;当没有任何变量引用该地址时,该地址对应的数据空间成为一个垃圾,由GC来回收。
  • 栈和堆的示意图


4 标识符

4.1 空标识符( _ )

​ 空标识符可以表示其他的标识符,但是其对应的值会被忽略(可用于忽略函数的某个返回值)。

​ 仅能作为占位符使用,不能作为标识符使用。


4.2 保留关键字(25个)

不能使用保留关键字作为标识符(共25个)


4.3 预定义标识符(36个)


4.4 标识符命名的注意事项

  1. 包名:保持 package 的名字和目录保持一致,简短但有意义,不要和标准库冲突(如 fmt )。

  2. 变量名、函数名、变量名:使用驼峰命名法

    • 若变量名、函数名、变量名的首字母大写,则可以被其他的包访问(公开的);

      若小写,则只能在当前包中使用(私有的)。

      (Go 中没有 public 和 private 关键字)


5 运算符

5.1 位运算符

运算符 功能 运算规则
& 参与运算的两数个对应的二进位相与 只有同时为1时,结果为1
` ` 参与运算的两数个对应的二进位相或
^ 参与运算的两数个对应的二进位相异或 二进位不同时为1,否则为0
<< 把左边的运算数各二进制位全部左移若干位 高位丢弃,低位补0;
左移 n 位即乘 \(2^n\)
>> 把左边的运算数各二进制位全部右移若干位 左补 n 个符号位,
右移 n 位即除以 \(2^n\)

5.2 运算符的优先级


5.3 注意事项

  1. 除号 /:在整数之间做除法运算时,结果只保留整数部分。

  2. 自增、自减运算符 ++ --

    • 只能作为独立的表达式使用。

      以下是错误例

      a = i++ // 错误
    • 只能写在变量的后面。

  3. Go 语言不支持三目运算符


6 控制台输入

6.1 方法

  1. 使用 fmt 包中的 Scanln(换行时结束扫描) , Scanf 函数

    例:

    var name string
    fmt.Scanln(&name)
    fmt.Scanf("%s", &name)

7 程序结构

顺序、分支、循环

7.1 分支 if switch

  1. if 语句

    if expression {
    statement
    } else if expression {
    statement
    } else {
    statement
    }
  2. switch 语句

    switch expression {
    case exp1, exp2, ... :
    statement
    case exp3, exp4, ... :
    statement
    default:
    statement
    }
    • switch 的 case 中,不用再加 break

    • switch 和 case 后是一个表达式(可以是常量、变量、带有返回值的函数等)。

      • switch 后的表达式可以为空,此时可以类似 if-else 语句形式。
    • switch 穿透:可以在 case 中的 statement 末尾加 fallthrough ,执行下一个 case 中的语句。

    • Type Switch:switch 语句可以用于 type-switch ,用以判断某个 interface 变量中实际指向的变量类型。

      例:

      var x interface{}
      // var y = 10.0
      // x = y
      switch i := x.(type) {
      case nil:
      fmt.Print("x的类型是: %T", i)
      case int:
      fmt.Print("x: int")
      case float64:
      fmt.Print("x: float64")
      case func(int) float64:
      fmt.Print("x: func(int)")
      default:
      fmt.Print("unknown")
      }
  3. if 语句和 switch 语句的比较

    • switch 语句:判断的具体数值不多,且符合整型、浮点型、字符型、字符串型,建议使用。
    • if 语句:其他情况,如区间判断,结果为 bool 型的判断。if 语句的适用范围更广。

7.2 循环 for

7.2.1 for 和 for-range

  • for 语句

    for 循环变量初始化; 循环控制条件; 循环变量迭代 {
    statement
    }
    // 类似while循环的形式
    for {
    statement
    }
  • for-range 语句

    • 可用于遍历字符串和数组。

      例:

    var str string = "Hello World!"
    for index, val := range str {
    statement
    }
  • 两种语句的区别

    1. for 语句按照字节对字符串进行遍历,如果字符串含有中文,可能会出现乱码的情况。解决方法:将 str 转成 []rune 切片。

      而 for-range 语句按照字符进行遍历,没有上面的问题。


7.2.2 break continue 和 goto return 语句

  • 带标签的 break 语句

    break 出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块。

    例:

    label1:
    for i := 0; i < 4; i++ {
    for j := 1; j < 10; j++ {
    break label1
    }
    fmt.Println("j = ", j)
    }

  • 带标签的 continue 语句

    结束本次循环,进入指定的下一次循环。


  • goto 语句(不建议使用):跳转控制语句,跳转至指定标签

    语法:

    goto label // 执行到此行时跳转至label所在行,继续执行程序
    statement
    label:
    statement

  • return 语句:用于跳出所在的方法或函数。

    • 如果在 main 函数中,表示终止 main 函数,即终止程序。
    • 如果在其他函数中,表示跳出该函数。

8 函数、包和错误处理

8.1 包

  • 含义:包的本质实际上就是创建不同的文件夹,用以存放程序文件。

  • 作用:

    1. Go 语言以包的形式来管理文件和项目目录结构。
    2. 三大作用
      • 区分相同名字的函数、变量等标识符。
      • 当程序文件很多时,可以很好地管理项目。
      • 控制函数、变量等访问范围,即作用域。
  • 打包的方法:

    package pkgName

  • 引入包的基本用法:

    import "包的路径"

    引入多个包:

    import (
    "pkg1"
    "pkg2"
    )

    路径从 $GOPATH 的 src 下开始(不用带 src)。

  • 注意事项

    1. 文件的包名通常和文件所在的文件夹名一致,一般为小写字母。

    2. 使用别的包的函数或变量,需要先引入相应的包。

    3. package 在前,import 在后。

    4. 跨包访问:需要调用的函数首字母大写。

    5. 取别名:可以给包取别名。但是取别名后,原来的包名就不能使用。(包名较长时可以使用)

      例:

      package main
      import (
      "fmt"
      util "go_packages/util"
      )
    6. 同一个包下,不能有相同的函数名和相同的全局变量名。否则会有重复定义的报错。

    7. 如果需要编译成可执行程序文件,需要将该包声明为 package main。写库则名可以自定义。


8.2 函数

语法:

func funcName (args) (返回值类型列表) {
// statement
return 返回值列表
}

8.2.1 init 函数

每个源文件都可以包含一个 init 函数,该函数会在执行 main 函数之前被 Go 的运行框架调用。

主要完成初始化工作。

  • 注意事项

    1. 如果一个文件同时包含全局变量定义, init 函数和 main 函数。

      则执行的顺序为:全局变量定义 → initmain


8.2.2 匿名函数

只希望使用一次时考虑使用。匿名函数也可以实现多次调用。

8.2.2.1 使用方法

  • 使用方法 1:定义时。该匿名函数只能调用一次。

    res1 := func (n1 int, n2 int) int {
    return n1 + n2
    }(10, 20)
  • 使用方法 2:将匿名函数赋给一个变量。可通过多次调用变量来实现多次调用匿名函数。

    a := func (n1 int, n2 int) int {
    return n1 + n2
    } res2 := a(10, 20)

8.2.2.2 全局匿名函数

即将匿名函数赋给一个全局变量。


8.2.3 return 语句

Go 预言的函数支持返回多个值。

  1. 在函数返回多个值时,如果不需要某个值,可用 _ 代替。

  2. 返回值只有一个时,返回值类型列表 处可以不写 ()

  3. 可以对返回值进行命名

    例:

    func getSumAndSub(n1 int, n2 int) (sum int, sub int) {
    sub = n1 - n2
    sum = n1 + n2
    return
    }

8.2.4 注意事项

  1. 形参列表和返回值列表的数据类型可以是值类型和引用类型。

  2. 函数名首字母为大写,则可被别的包的调用。

  3. 基本数据类型数组默认都是值传递的,即进行值拷贝,不会影响原来的值。

    如果希望调用的函数修改原有的值,则可进行引用传递。

  4. Go 不支持函数重载

  5. 函数也是一种数据类型,可赋值给一个变量。通过该变量可以对函数进行调用

    例:

    因此函数也可作为形参

    例:

    func myFunc(funvar func(int, int) int, num1 int, num2 int) int {
    return funvar(num1, num2)
    }
  6. Go 支持可变参数

    func sum1(args... int) sum int {
    // statement
    }
    func sum2(n1 int, args... int) sum int {
    // statement
    }
    • args 是 slice 切片,通过 args[index] 可以访问各个值。
    • 形参列表中有可变参数,需要放在形参列表的最后。

8.2.5 闭包

  • 含义:闭包是一个函数与其相关的引用环境组合的一个整体(实体)。

    例:

  • 说明:

    1. 返回的是一个匿名函数。该匿名函数引用了其外部的函数的变量(suffix)。因此该匿名函数和 suffix 形成一个整体,构成闭包。
    2. 反复调用 f2 时,suffix 仅初始化一次。

8.2.6 函数的延时机制 defer

  • 作用:在函数执行完毕后,及时释放资源。

  • 使用:

    1. 当执行到一个 defer 时,不会立即执行其后的语句,而是将其后的语句压入的一个独立的栈中,然后继续执行下一个语句。

    2. 当函数执行完毕后,会在该独立的栈中,从栈顶去除语句执行。(遵守栈 先入则后出的机制)。

    3. 入栈时,会将相关的值拷贝,同时入栈。

      例:

      n1 := 1
      defer fmt.Println(n1) // 输出为 1
      n1++
  • 流程:

    1. 通常的做法是,在创建资源后(如打开了文件,获取了数据库的连接,或者是锁资源),可以执行 defer file.close()defer connect.close() 等。
    2. defer 后,可以继续使用创建的资源。
    3. 当函数执行完毕后,系统会依次从独立栈中取出语句,关闭资源。

8.2.7 函数的作用域

赋值语句不能在函数体外


8.2.8 常用函数

8.2.8.1 字符串常用函数

  1. 字符串的长度(按字节)len(str)

  2. 字符串遍历(处理有中文的问题)r := []rune(str)

  3. 字符串转整数 n, err = strconv.Atoi("12")

    整数转字符串 str = strconv.Itoa(12)

  4. 字符串转 []bytevar bytes = []byte("Hello World")

    []byte 转字符串:str = string([]byte{97, 98, 99})

  5. 10进制转2、8、16进制:str = strconv.FomatInt(num, base)

  6. 查找子字符串是否在指定的字符串中:strings.Contains("seafood", "food") // true

  7. 统计一个字符串有几个指定的字符串:strings.Count("what's your name?", "a") // 2

  8. 字符串比较(不区分大小写) strings.EqualFold("abc", "Abc")

  9. 返回字符串第一次出现的 index 值,如果没有则返回 -1 strings.Index("efg_abc", "abc") // 4

    返回字符串最后一次出现的 index 值,如果没有则返回 -1 strings.LastIndex("go golang", "go") // 3

  10. 将指定的子字符串替换为另一个子串 strings.Replace("go golang", "go", "go language", n) // n表示替换几个,若为-1则全部替换

  11. 拆分成字符串数组(以某个指定的字符为分隔标识) strings.Split("How are you?", " ")

  12. 大小写转换 strings.ToLower("GO") strings.ToUpper("go")

  13. 去掉指定的字符

    去掉左右的空白符 strings.TrimSpace

    去掉左右两边指定的字符 string.Trim("!g!o!", "!") // "g!o"

    去掉左边或右边的指定字符 string.TrimLeft("!g!o!", "!") strings.TrimRight("!g!o!", "!")

  14. 判断字符串是否以指定的字符串开头 strings.HasPrefix("http://", "http")

    判断字符串是否以指定的字符串结尾 strings.HasSuffix("winter.jpg", ".jpg")


8.2.8.2 时间、日期相关的函数

导入 time 包。

  1. time.Time 类型,用于表示时间。

    now := time.Now() // 获取当前时间
  2. 通过 now 可以获取年月日、时分秒。

    now.Year()
    now.Hour()
  3. 格式化时间。

    [方法 1] Printf 或 SPrintf。

    [方法 2] time.Format() 方法。其中,"2006-01-02 15:04:05" 的数字固定的。可以根据需要自由组合。

    now.Format("2006-01-02 15:04:05")
  4. 时间的常量

    const (
    Nanosecond = 1
    Microsecond = 1000 * Nanosecond
    Millisecond = 1000 * Microsecond
    Second = 1000 * Millisecond
    Minute = 60 * Second
    Hour = 60 * Minute
    )
  5. time 的 Unix 和 UnixNano 方法


8.2.8.3 内置函数

Go语言标准库文档中文版

  1. new:用于分配内存,主要用来分配值类型,如 int, float32, struct 返回的是指针。
  2. make:用于分配内存,主要用来分配引用类型,如 channel, map, slice。

8.3 错误处理

处理方式 defer, panic, recover。Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。

8.3.1 自定义错误

使用 errors.Newpanic 内置函数。

  1. errors.New("错误说明"),返回一个 error 类型的值,表示一个错误。
  2. panic 内置函数,接受一个 interface{} 类型的值(即任何值)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序

9 数组与切片

  • 在 Go 中,数组是值类型,切片是引用类型。

9.1 数组

9.1.1 数组的基本使用

  • 数组定义:var arrayName [size]int

  • 数组的内存布局:

    1. 数组首元素的地址就是数组的首地址。

    2. 数组各元素之间的地址间隔根据数组的类型决定,如 int64 是 8,int32 是 4。

  • 数组的遍历:

    [方式 1] 常规遍历

    [方式 2] for-range 遍历

    for index, value := range array01 {
    // statement
    }
    • 如果不需要下标,可用 _ 代替。
  • 注意事项:

    1. 数组的长度一旦声明或定义,不可动态变化。
    2. 数组创建后,如果没有赋值,有默认值。默认值同上。
    3. 在进行函数传参时,需要考虑数组的长度。

9.2.2 多维数组

  • 定义语法:

    var 数组名 [大小][大小]类型
    // 如:var arr [2][3]int
  • 使用方法:

    1. 先声明/定义,再赋值。

    2. 直接初始化。

      var arr3 [2][3]int = [2][3]int{{1, 2, 3}, {4, 5, 6}}
  • 遍历方法:

    1. 两层 for 循环。
    2. 两层 for-range 循环。

9.2 切片 slice

9.2.1 切片的基本使用

  • 切片是数组的一个引用,因此切片是引用类型。

  • 使用和数组类似。

  • 切片的长度可以变化,即可认为切片是一个可以动态变化的数组

  • 语法:

    var 切片名 []类型
    // 如 var a []int
  • 内存布局:

    1. slice 从底层来看,是一个数据结构(struct 结构体)

      type slice struct {
      ptr *[2]int
      len int
      cap int
      }

  • 切片的使用:

    1. [方式 1]:定义一个切片,之后让切片引用一个已经创建的数组。

      var arr [5]int = [...]int {1, 2, 3, 4, 5}
      var slice = arr[1:3]
    2. [方式 2]:语法如下,

      var 切片名 []type = make([]type, len, [cap])
      • type 数据类型,len 大小,cap 切片容量。

      • cap 可选,如果选择 cap, 则 cap 必须 ≥ len。

      • 例:

        var slice []float64 = make([]float64, 5, 10)

  • 切片的遍历

    类似于数组。


  • 增加元素:append() 函数

  • 切片的拷贝:copy() 函数

    copy(slice1, slice2) // 将slice2的元素依次赋给slice1

9.2.2 String 与切片

  • string 底层是一个 byte 数组,因此 string 也可进行切片处理。
  • string 是不可变的。如果需要修改字符串:
    1. 可以先将 string 型转换成 []byte 型。
    2. []rune -> 修改 -> 重写转成 string。

10 map

map 是 key-value 数据结构,又称为字段或者关联数组。类似于其他编程语言的集合。

10.1 map 的基本使用

10.1.1 map 的声明

  • 语法:

    var map 变量名 map[keyType]valueType
    • key 不可为 slice, map 和 function,因为这几个无法用 == 来判断。

      key 通常为 int 或者 string 型。

    • valueType 的类型和 key 的基本一样。

      key 通常为数字(整数、浮点数),string,map,struct。

    • 例(map 的声明):

      var a map[string]string
      var b map[int]string
      • 声明不会分配内存,需要 make 初始化后才可赋值和使用。
    • 注意事项:

      1. map 的 key 不能重复。如果重复了,则以最后的为准。
      2. map 的 keyValue 是无序的。

10.1.2 map 的基本使用

  • [方式 1]

    var a map[string]string
    a = make(map[string]string, 10)
    a["no1"] = "test1"
    a["no2"] = "test2"
  • [方式 2]

    a := make(map[string]string)
    a["no1"] = "test1"
    a["no2"] = "test2"
  • [方式 3]

    a := map[string]string {
    "no1" : "test1"
    "no2" : "test2"
    }

10.1.3 map 的增删改查 和 遍历

  • 增加和更新

    mapName["key"] = value // key还不存在就是增加,存在则是更新
  • 删除

    delete(mapName, "key")
    // 如果key存在则删除该keyValue,否则不操作(不会报错)

    删除所有 key 的方法:① 遍历; ② 直接 make() 一个新的,原来的key成为垃圾,被 go 回收。

  • 查找

    例:

    val, exist := a["no1"]
    if exist {
    // statement
    } else {
    // statement
    }

  • 遍历

    使用 for-range 的结构遍历。


10.1.4 map 切片、排序

  • 切片

    如果切片的数据类型是 map,则称其为 map 切片。这样,map 的个数就可以动态变化了。

    • 例:

      // 声明
      var monsters []map[string]string
      monsters = make([]map[string]string, 2)
      // ......
      newMonster := map[string]string {
      "name" : "unknown"
      "age" : "200"
      }
      monsters = append(monsters, newMonster)

  • 排序

    go 中的 map 默认是无序的,也不按照添加的顺序存放(因此每一次遍历得到的结果顺序可能不一样)。

    可以先对 key 排序,然后根据 key 值遍历输出。


10.1.5 注意事项

  1. [动态] map 的容量达到后,再增加元素,会自动扩容(不会发生 panic)。
  2. map 的 value 经常使用 struct 类型,更适合管理复杂的数据(优于 value 类型是 map 的情况)。

11 结构体 (面向对象)

  • 结构体是值类型。不同的结构体变量的字段相互独立。

  • Go 基于结构体 struct 实现面向对象。

  • 通过 匿名字段 实现继承。

11.1 结构体的基本使用

11.1.1 声明和创建结构体变量

  • 声明语法:

    type 结构体名称 struct {
    field1 type
    field2 type
    }
  • 字段(属性, field),方法。

  • 创建结构体变量

    1. [方式 1] 直接声明:var person Person
    2. [方式 2] {}:var person Person = Person{}
    3. [方式 3 返回指针] &:var person *Person = new(Person)
    4. [方式 4 返回指针] {}:var person *person = &Person
  • 通过指针访问结构体字段的方法:(*person).Name = "Tom"person.Name

  • 内存分配机制:

    1. 结构体的所有字段在内存中是连续的。

11.1.2 字段(属性, field)

  1. 在创建结构体变量之后,字段会被赋默认值

  2. 和其他类型进行转换时需要有完全相同的字段(名字、个数和类型)。

  3. 对结构体进行 type 重新定义相当于取别名,但 Go 认为取别名后的是新的数据类型。相互间可以通过通过显式转换

    例:

    type Student struct {
    Num int
    }
    type Stu Student
  4. struct 的每个字段上,可以写一个 tag,该 tag 可以 通过反射机制获取。常见的使用场景为序列化和反序列化。

    例:

    type Student struct {
    Name string `json:"name"`
    Age int `json:"age"`
    }
    // 1 创建一个Student变量
    stu1 := Student{"stu1", 20}
    // 2 将Student变量序列化为json格式字符串
    jsonStr, err := json.Marshal(stu1)

11.1.3 方法

  • 声明的语法:

    type A struct {
    Num int
    }
    func (a A) test() {
    // statement
    fmt.Prinln(a.Num)
    }
    • (a A) 体现 test 方法和 A 类型绑定。
    func (receiver type) methodName (参数列表) (返回值列表) {
    // 函数体
    return 返回值
    }
    • receivertype 进行绑定。

      receivertype 类型的一个变量(实例)

      type 可以是结构体,也可以是其他自定义类型

    • return 语句不是必须的

  • 方法的调用和传递机制

    1. 变量调用方法时,该变量本身也会作为一个参数传递到方法中。

      如果变量是值类型,则进行值拷贝;如果是引用类型,则进行地址拷贝。

  • 注意事项

    1. 如果想要在方法中修改结构体变量的值,可以通过结构体指针的方式处理。
    2. Go 中的方法作用在指定的数据类型上(即和指定的数据类型绑定),因此自定义类型都可以有方法
    3. 注意:方法的首字母大写则可在包外访问,否则只能在包内访问。
    4. 如果一个类型有 String() 方法,那么 fmt.Println() 方法会默认调用该变量的 String() 方法进行输出。

11.1.4 函数和方法的区别

  1. 调用方式不同。

  2. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。

    对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法,反之亦然。


11.2 工厂模式

即定义一个函数,起到和构造函数类似的作用。(Go 没有构造函数)

如对于 student 结构体,有

func NewStudent(n string, a int) *student {
return *student{
name: n,
age: a,
}
}

之后,就可以使用该函数在别的包下创建该结构体的实例(先导入)。

  • 该函数返回的是指向该结构体实例的指针。

12 面向对象

12.1 封装、继承、多态

12.1.1 封装

[首字母小写]

将结构体、字段的首字母小写。

[首字母大写]

提供一个工厂模式的函数。

提供 Set 和 Get 方法。


12.1.2 继承

继承

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

例:

type Goods struct {
Name String
Price int
}
type Book struct {
Goods // 嵌套匿名结构体 Goods
Writer string
}
  • 注意事项:

    1. 封装并创建实例 book 后,以上面的代码为例,可以通过 book.Goods.Namebook.Name 访问 Name 字段。

    2. 当结构体和匿名结构体有相同的字段或方法时,编译器遵从就近访问原则。若需要匿名函数体重名的字段和方法,可以通过如 book.Goods.Name 的方式实现。

    3. 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身 没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。

    4. 组合:一个结构体嵌套了一个有名结构体

      • 在访问组合的结构体的字段或方法时,必须带上有名结构体的名字

        type D struct {
        a A
        }
        var d D
        d.a.Name = "abc"
    5. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值。


多重继承

如果 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套匿名结构体的字段和方法。

尽量不使用多重继承。


12.1.3 多态

12.1.3.1 接口 interface

  • 语法

    type 接口名 interface {
    method1(参数列表) 返回值列表
    method2(参数列表) 返回值列表
    ...
    }
    // 实现接口的所有方法
    func (t 自定义类型) method1(参数列表) 返回值列表 {
    // 方法实现
    }
    func (t 自定义类型) method2(参数列表) 返回值列表 {
    // 方法实现
    }
  • 说明:

    1. 接口中,所有的方法都没有方法体。即接口的方法都是没有实现的方法。
    2. Go 中的接口,不需要显示的实现。一个变量,若含有接口类型中的所有方法,那么这个变量就是实现这个接口。(Go 中没有 implement 关键字)
  • 注意事项

    1. 接口本身不能创建实例,但是可以指向一个实现了改接口的自定义类型的变量(即实例)。
    2. 一个自定义类型需要将某个接口的所有方法都实现,则认为这个自定义类型实现了该接口。
    3. 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型。
    4. 只要是自定义数据类型就可以实现接口。
    5. 一个自定义类型可以实现多个接口。
    6. 接口中不能含有任何变量
    7. 一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必须将 B,C 接口的方法全部实现。
    8. interface 类型默认是一个指针,若未经初始化使用,则会输出 nil
    9. 空接口没有实现任何方法,因此所有类型都实现了空接口可以把任一变量赋给空接口

12.1.3.2 多态

多态特征通过接口实现。

  • 接口体现多台的两种形式

    • 多态参数

      即可以接收 A 类型的变量,又可以接收 B 类型的变量。

    • 多态数组


12.2 类型断言

将接口转为具体类型时,需要使用类型断言,如下:

var x interface {}
var b2 = float32 =1.1
x = b2 // 看空接口可以接收任何类型
// 使用类型断言
y := x.(float32)
  • 说明:

    在进行类型断言时,如果类型不匹配,就会报 panic,因此进行类型断言时,要确保原来的空接口指向的就是断言的类型。

  • 带检测的类型断言

    var x interface {}
    var b2 = float32 =1.1
    x = b2 // 看空接口可以接收任何类型
    // 使用类型断言(带检测的)
    if y, ok := x.(float32); ok {
    // convert succeed
    } else {
    // convert fail
    }

13 文件操作

13.1 文件的读写

os.File 封装所有文件相关操作,File 是一个结构体

  • 打开和关闭文件

    func Open(name string) (file *File, err error)
    func (f *File) Close() error
  • 应用实例

    1. 读取文件的内容并显示在终端(带缓冲区的方式),使用 os.Open, file.Close, bufio.NewReader(), reader.ReadString 函数和方法。

      例:

      func main() {
      file, err := os.Open("test.txt")
      if err != nil {
      fmt.Println("Open file error:", err)
      return
      } defer file.Close() reader := bufio.NewReader(file)
      for {
      str, err := reader.ReadString('\n')
      if err == io.EOF {
      break
      }
      fmt.Println(str)
      }
      fmt.Println("文件读取结束")
      }
    2. 读取文件的内容并显示在终端(使用 ioutil 一次将整个文件读入到内存中),这种方式适用于文件不大的情况。相关方法和函数(ioutil.ReadFile)


    3. os.OpenFile 函数

      func OpenFile(name string, flag int, perm FileMode) (file *File, err error)

      说明:os.OpenFile 是一个更一般性的文件打开函数,它会是使用指定的选项(如O_RDONLY 等)、指定的模式(如 0666 等)打开指定的文件。

      • 如果操作成功,则返回的文件对象可用于 I/O;

      • 如果出错,错误的底层类型是 PathError。

      • 形参说明:

        1. flag 指的是文件打开模式(可以组合)。

        2. perm 指的是权限控制(linux / unix)。

          r -> 4
          w -> 2
          x -> 1
      • 因为 writer 是带缓存,因此在调用 WriterString 方法时,其实 内容是先写入到缓存的,所以需要调用 Flush 方法,将缓冲的数据真正写入到文件中, 否则文件中会没有数据。

    4. 编写一个程序,将一个文件的内容,写入到另外一个文件。

      使用 ioutil.ReadFile / ioutil.WriteFile 完成。

      filename := "C:/Users/Jackkee/Desktop/test.txt"
      
      content, err := ioutil.ReadFile(filename)
      if err != nil {
      fmt.Println("ReadFileError:", err)
      return
      } filename2 := "C:/Users/Jackkee/Desktop/newTest.txt"
      err = ioutil.WriteFile(filename2, content, 0666)
      if err != nil {
      fmt.Println("WriteFileError:", err)
      return
      }

  • 判断文件是否存在

    使用 os.Stat() 函数返回的错误值进行判断:

    1. 如果返回的错误为 nil, 则文件或文件夹存在。
    2. 如果错误类型使用 os.IsNotExist() 判断为 true,说明文件或文件夹不存在。
    3. 如果返回的错误为其他类型,则不确定是否存在。
    func PathExists(path string) (bool, error) {
    _, err := os.Stat(path)
    if err == nil {
    return true, nil
    }
    if os.IsNotExist(err) {
    return false, nil
    }
    return false, err
    }

13.2 命令行参数

  • os.Args 是一个 string 的切片,用于存储所有的命令行参数。

  • 使用 flag 包来解析命令行参数。

    例:

    func main() {
    var user string
    var pwd string
    var host string
    var port int
    // &user 就是接受用户命令行中输入的-u后的参数值。
    // "u" 就是 -u 制定参数
    // "" 默认值
    // "用户名,默认为空" 说明
    flag.StringVar(&user, "u", "", "用户名,默认为空")
    flag.StringVar(&pwd, "ped", "", "密码,默认为空")
    flag.StringVar(&host, "h", "localhost", "主机名,默认为localhost")
    flag.IntVar(&port, "port", 3306, "端口号,默认为3306")
    // 必须使用下面的方法,进行转换
    flag.Parse() fmt.Printf("user=%s, pwd=%s, host=%s, port=%d", user, pwd, host, port) }

13.3 json

{"key1":val1, "key2":val2}

13.3.1 json 的序列化

指将有 key-value 结构的数据类型(结构体、map、切片等)序列化成 json 字符串。

  • 序列化结构体

    type Student struct {
    // 给字段添加tags
    Name string `json:"name,omitempty"`
    Age int `json:"age,omitempty"`
    Habit string `json:"habit,omitempty"`
    } func struct2Json() {
    stu := Student{
    Name: "test",
    Age: 10,
    Habit: "singing",
    }
    data, err := json.Marshal(stu)
    if err != nil {
    fmt.Println("Error: ", err)
    }
    fmt.Println(string(data))
    }
    • 对结构体进行序列化时,字段名首字母应大写

  • 序列化 map

    func map2Json() {
    var testMap map[string]interface{}
    testMap = make(map[string]interface{})
    testMap["name"] = "testMap1"
    testMap["age"] = 18
    data, err := json.Marshal(testMap)
    if err != nil {
    fmt.Println("序列化错误:", err)
    }
    fmt.Println(string(data))
    }

  • 序列化 slice

    func slice2Json() {
    var slice2 []map[int]interface{}
    var map2 map[int]interface{}
    map2 = make(map[int]interface{})
    map2[0] = "i'm sorry"
    map2[1] = "the old taylor can't come to the phone right now"
    slice2 = append(slice2, map2)
    data, err := json.Marshal(slice2)
    if err != nil {
    fmt.Println(err)
    }
    fmt.Println(string(data))
    }

  • 序列化基本数据类型

    func float64ToJson() {
    // 对基本数据类型进行序列化意义不大
    var num1 = 123.456
    data, err := json.Marshal(num1)
    if err != nil {
    fmt.Println(err)
    }
    fmt.Println(string(data))
    }

13.3.2 json 的反序列化

指将 json 字符串转换成对应的数据类型(如结构体、map、切片)。

  • json 反序列化为 struct

    type Student struct {
    Name string
    Age int
    Height float64
    } func json2Struct() {
    str := "{\"Name\":\"test\",\"Age\":18,\"Height\":180.0}"
    var stu Student
    // v interface{}
    // 如果 v 是 nil 或者 v 不是指针,该方法返回一个错误(InvalidUnmarshalError)
    // If v is nil or not a pointer, Unmarshal returns an InvalidUnmarshalError.
    err := json.Unmarshal([]byte(str), &stu)
    if err != nil {
    fmt.Println(err)
    }
    fmt.Println(stu)
    }
    • Unmarshal 方法的第二个参数 v 如果是 nil 或者 v 不是指针,该方法返回一个错误(InvalidUnmarshalError)。

      即 v 必须是指针类型。


  • json 反序列化为 map

    func json2Map() {
    str := "{\"Name\":\"test\",\"Age\":18,\"Height\":180.0}"
    var testMap map[string]interface{}
    err := json.Unmarshal([]byte(str), &testMap)
    if err != nil {
    fmt.Println(err)
    }
    fmt.Println(testMap)
    }
    • 反序列化 map 不需要 make 函数。因为 make 函数的操作已经被封装到 Unmarshal 方法中。

  • json 反序列化为 slice

    func json2Slice() {
    str := "[{\"Name\":\"test\",\"Age\":18,\"Height\":180.0}]"
    var slice []map[string]interface{} err := json.Unmarshal([]byte(str), &slice)
    if err != nil {
    fmt.Println(err)
    }
    fmt.Println(slice)
    }

  • 说明

    1. 在反序列化 json 字符串时,要哦确保反序列化后的数据类型和原来序列化之前的数据类型保持一致。
    2. 如果 json 字符串是通过程序得到的,则不需要再对 " 转移处理,即不用在 " 前加 \

14 单元测试

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

可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。

  • 说明

    • 测试用例的文件名必须以 _test.go 结尾。

    • 测试用例的函数必须以 Test 开头。

    • 一个测试样例中,可以有多个测试用例函数。


    • 运行测试用例的指令:

      go test
      // 如果运行正确,无日志;错误时会输出日志
      go test -v
      // 正确或错误都会输出日志
    • 相关的方法:

      1. 出现错误时,可以用 t.Fatalf 格式化输出错误信息,并退出程序。
      2. t.Logf 方法可以输出相应的日志。

    • 测试用例函数可以不在 main 函数中运行。

    • PASS 表示测试用例运行成功,FAIL 表示运行失败。

    • 测试单个文件时,一定要带上被测试的原文件。

      go test -v cal_test.go cal.go
    • 测试单个方法

      go test -v -test.run TestAddUpper

15 goroutine 和 channel

15.1 并发和并行

  • 多线程程序在单核上运行,就是并发

  • 多线程程序在多核上运行,就是并行


  • 各自的特点:

    • 并发

      1. 多个任务作用在一个 cpu。
      2. 从微观的角度看,在一个时间点上,只有一个任务在执行。
    • 并行
      1. 多个任务作用在多个 cpu。
      2. 从微观的角度看,在一个时间点上,有多个任务在同时执行。
      3. 并行的速度相对较快。

15.2 Go 协程和 Go 主线程

  • Go 主线程:一个 Go 线程上,可以起多个协程协程是轻量级的线程[编译器做优化]。

  • Go 协程的特点

    1. 有独立的栈空间。
    2. 共享程序堆空间。
    3. 调度由用户控制。
    4. 协程是轻量级的线程。

    示意图如下:

    实例程序:

    func test() {
    for i := 0; i< 10; i++ {
    fmt.Println("test() Hello World" + strconv.Itoa(i))
    time.Sleep(time.Second)
    }
    } func main() {
    // 开始协程
    go test() for i := 0; i < 10; i++ {
    fmt.Println("main() Hello Golang" + strconv.Itoa(i))
    time.Sleep(time.Second)
    }
    }
    • 执行流程图

  • 小结

    • 主线程是一个物理线程,直接作用于 CPU。重量级,非常耗费 CPU 资源。
    • 协程是从主线程开启的,是轻量级的线程。逻辑态,对资源耗费相对较小。
    • (Go 在并发上的优势)Go 的协程可以开启上万个协程。

15.2.1 goroutine 的调度模型

15.2.1.1 MPG 模式

M -> 操作系统的主线程
P -> 协程执行需要的上下文
G -> 协程
  • 运行状态 1:

  • 运行状态 2:


15.2.1.2 [go 1.8 以前]设置 go 运行的 CPU 数

num := runtime.NumCPU
runtime.GOMAXPROCS(num)
  • go 1.8 以后,默认使程序运行在多个核上,可以不用设置。
  • go 1.8 以前需要设置,以获取更高的运行效率。

15.2.2 管道 channel

加锁 lock

  • 不同 goroutine 之间的通讯方式

    1. 全局变量的互斥锁。

      没有对全局变量 m 加锁,会出现资源争夺问题,提示 concurrent map writes。

    2. 使用 channel 解决。


  • 使用全局变量加锁同步改进程序。

    lock.Lock() // 加锁
    mapTest[n] = ans
    lock.Unlock() // 解锁
    • 缺点:

      1. 主线程等待所有 goroutine 全部完成的时间很难确定。

        如果主线程休眠时间过长,会增加等待时间;

        如果果断,可能还有 goroutine 处于工作状态,此时它们会随着主线程的退出而销毁。

      2. 使用全局变量加锁同步来实现通讯,不利于利用多个协程对全局变量的读写操作。


channel 的基本信息

  • 本质是一个数据结构队列,示意图如下。

  • 数据 先进先出(FIFO: First In First Out)

  • channel 本身是线程安全的:多 goroutine 访问时,无需加锁。

  • channel 有类型,一个 string 型的 channel 只能存放 string 型数据

  • 示意图:


定义与声明

var 变量名 chan 数据类型
// 如:var intChan chan int
  • 说明:

    • channel 是引用类型。
    • channel 必须初始化才可写入数据,即 make() 后才可使用。
    • channel 具有类型,只能写入指定数据类型。

初始化、写入数据至 channel、从 channel 读取数据及其注意事项

  • 例:

    var intChan chan int
    // 创建一个可以存放3个int型值的channel
    intChan = make(chan int, 3)
    fmt.Printf("intChan的值=%v, 本身的地址=%p\n", intChan, &intChan) // 向管道中写入数据,不可超过其容量(cap)
    intChan <- 10
    num := 211
    intChan <- num
    intChan <- 50 fmt.Printf("channel len=%v, cap=%v\n", len(intChan), cap(intChan)) // 读取 channel 中的数据(取出)
    var num2 int
    num2 = <-intChan
    fmt.Println("num2=", num2)
    fmt.Printf("channel len=%v, cap=%v\n", len(intChan), cap(intChan)) // 在没有使用协程的情况下,若管道中的数据已经全部取出,再取就会报告 deadlock
    num3 := <-intChan
    num4 := <-intChan
    num5 := <-intChan // fatal error: all goroutines are asleep - deadlock!
    fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)

    注意事项
  • channel 中只能存放指定的数据类型

    将类型指定为 interface{},就可以实现任意数据类型的存放

  • channel 的数据不能超过其最大容量。

    但从 channel 中取出数据后,可以继续放入数据。

  • 在没有使用协程的情况下,如果 channel 数据取完时再取,会报错(deadlock):

    fatal error: all goroutines are asleep - deadlock!

  • interface{} 型的 channel 中取出数据时,需要进行类型断言。

    例:

    var interfaceChan chan interface{}
    interfaceChan = make(chan interface{}, 3)
    cat1 := Cat{Name:"tom", Age:10,}
    cat2 := Cat{Name: "jerry", Age:10,}
    interfaceChan <- cat2
    interfaceChan <- cat1
    interfaceChan <- 10
    // 取出
    newCat := <- interfaceChan
    a := newCat.(Cat) // 类型断言。不可以直接newCat.Name
    fmt.Println("newCat.Name=",a.Name)

遍历和关闭

  • channel 的关闭

    使用 close() 可以关闭 channel。

    在关闭 channel 后,就不可再向 channel 写入数据但仍然可以从中读取数据


  • channel 的遍历

    channel 支持以 for-range 方式进行遍历。

    • 注意:

      1. 在遍历时,如果 channel 没有关闭,则会出现 deadlock 的错误。
      2. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后就会推出遍历。

    • 应用 1 (Page 442):

      func writeData(intChan chan int) {
      for i := 1; i <= 50; i++ {
      intChan <- i
      }
      close(intChan)
      } func readData(intChan chan int, exitChan chan bool) {
      for {
      v, ok := <-intChan
      if !ok {
      break
      }
      fmt.Println("readData读取到数据", v)
      }
      exitChan <- true
      close(exitChan)
      } func main() {
      var intChan = make(chan int, 50)
      var exitChan = make(chan bool, 1) go writeData(intChan)
      go readData(intChan, exitChan) for {
      _, ok := <-exitChan
      if !ok {
      break
      }
      }
      }

    • 应用 2 - 阻塞:

      如果只是向管道写入数据,而没有读取(编译器运行发现一个管道只有写,没有读,则改管道会阻塞。与读、写管道的速度快慢无关


    • 应用 3 - 运用协程和 channel 判断素数

      package main
      
      import "fmt"
      
      func putNum(intChan chan int) {
      for i := 1; i <= 100; i++ {
      intChan <- i
      }
      close(intChan)
      } func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) { for {
      isPrime := true
      num, ok := <-intChan
      // 如果 intChan 取不到
      if !ok {
      break
      }
      // 判断是否为质数
      if num == 1 {
      isPrime = false
      } else{
      for i := 2; i < num; i++ {
      if num%i == 0 {
      isPrime = false
      break
      }
      }
      }
      if isPrime {
      primeChan <- num
      }
      } fmt.Println("有一个协程因取不到数据而退出")
      exitChan <- true
      } func main() {
      intChan := make(chan int, 1000)
      primeChan := make(chan int, 2000) // 存放结果
      exitChan := make(chan bool, 4) // 退出标识 // 开启一个协程,向intChan放入数据
      go putNum(intChan)
      // 从 intChan 中取出数据,并判断是否为素数,若是则放入 primeChan 中
      for i := 0; i < 4; i++ {
      go primeNum(intChan, primeChan, exitChan)
      }
      go func() {
      // 判断是否完毕
      for i := 0; i < 4; i++ {
      <-exitChan
      }
      // 关闭管道
      close(primeChan)
      }() // 遍历 primeChan,把结果取出
      for {
      res, ok := <-primeChan
      if !ok {
      break
      }
      // 将结果输出
      fmt.Printf("素数:%d\n", res)
      }
      fmt.Println("主线程退出")
      }

使用细节和注意事项

  • channel 可以声明为只读,或者只写

    // 只写
    var chan1 chan<- int
    // 只读
    var chan2 <-chan int
  • 使用 select 可以解决从管道取数据的阻塞问题

    // 使用 select 可以解决从管道取数据的阻塞问题
    // 1.定义一个管道 10 个数据 int
    intChan := make(chan int, 10)
    for i := 0; i < 10; i++ {
    intChan<- i
    }
    // 2.定义一个管道 5 个数据 string
    stringChan := make(chan string, 5)
    for i := 0; i < 5; i++ {
    stringChan <- "hello" + fmt.Sprintf("%d", i)
    }
    // 传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
    // 问题,在实际开发中,可能我们不好确定什么关闭该管道.
    // 可以使用 select 方式可以解决
    // label:
    for {
    select {
    //注意: 这里,如果 intChan 一直没有关闭,不会一直阻塞而 deadlock,会自动到下一个 case 匹配
    case v := <-intChan :
    fmt.Printf("从 intChan 读取的数据%d\n", v)
    time.Sleep(time.Second)
    case v := <-stringChan :
    fmt.Printf("从 stringChan 读取的数据%s\n", v)
    time.Sleep(time.Second)
    default :
    fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")
    time.Sleep(time.Second)
    return
    //break label
    }
    }
  • goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题。

    解决方法:可以在 goroutine 中使用 recover 来捕获 panic 进行处理。这样即使这个协程发生问题,主线程不受影响,可以继续执行。

    func sayHello() {
    for i := 0; i < 10; i++ {
    time.Sleep(time.Second)
    fmt.Println("hello,world")
    }
    } func test1() {
    defer func() {
    // 捕获test抛出的panic
    if err := recover(); err != nil {
    fmt.Println("test()发生错误", err)
    }
    }()
    var myMap map[int]string
    myMap[0] = "golang" // error
    } func main() {
    go sayHello()
    go test1() for i := 0; i < 10; i++ {
    fmt.Println("main() ok=", i)
    time.Sleep(time.Second)
    }
    }

16 反射

import "reflect"

16.1 反射的 基本概念 和 使用场景

16.1.1 基本概念

  • 反射可以在运行时动态获取变量的各种信息。比如变量的类型(type),类别(kind)。

    如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)。

  • 通过反射,可以修改变量的值,可以调用关联的方法。


16.1.2 使用场景

  • 不知道接口调用哪个函数,根据传入参数在运行时确定调用的具体接口,这时需要对函数或方法反射。例如以下桥接模式:

    func bridge(funcPtr interface{}, args ...interface{})

    第一个 funcPtr 以接口的形式传入函数指针,函数参数以可变参数的形式传入,bridge 函数中可以通过反射来动态执行 funcPtr 函数。

  • 对结构体序列化时,如果结构体有指定 Tag,可以通过反射生成对应的字符串


16.1.3 相关函数和概念

  • reflect.TypeOf(变量名) ,获取变量的类型,返回 reflect.Type 类型。

  • reflect.ValueOf(变量名),获取变量的值,返回 reflect.Value 类型。

    • reflect.Value 是结构体类型。通过它可以获取到很多关于该变量的信息。
  • [常用] 变量、interface{} 和 reflect.Value 之间可以相互转换。示意图:


  • 例:

    type Student struct {
    Name string
    Age int
    } // 演示反射
    func reflectTest01(b interface{}) {
    // 通过反射获取的传入变量的 type 和 kind 值。
    // 1. 先获取 reflect.TypeOf(b)
    rType := reflect.TypeOf(b)
    fmt.Println("rType = ", rType) // 2. 再获取 reflect.Value
    rValue := reflect.ValueOf(b)
    n2 := 2 + rValue.Int()
    fmt.Println("n2 = ", n2)
    fmt.Printf("rVal=%v, rValue Type=%T\n", rValue, rValue) // 将 rVal 转成 interface{}
    itfValue := rValue.Interface()
    // 将 interface{} 通过类型断言转成需要的类型
    num2 := itfValue.(int)
    fmt.Println("num2 = ", num2) } func reflectTest02(b interface{}) {
    //rType := reflect.TypeOf(b)
    rVal := reflect.ValueOf(b) // 将 rVal 转换成 interface{}
    itfVal := rVal.Interface()
    fmt.Printf("itfVal = %v, type = %T\n", itfVal, itfVal) // 将 interface{} 通过断言转成需要的类型
    stu, ok := itfVal.(Student)
    if ok {
    fmt.Printf("stu.Name=%v\n", stu.Name)
    }
    } func main() {
    //var num = 100
    //reflectTest01(num)
    var stu = Student{
    Name: "test",
    Age: 20,
    }
    reflectTest02(stu)
    }

注意事项

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

  • Type 和 Kind 的区别

    Type 是具体的类型,Kind 是类别,两者不一定相同

    var num = 10 // num的Type和Kind都为int
    var stu Student // stu的Type是pkg.Student,Kind是struct
  • 通过反射可以在让变量在 interface{} 和 reflect.Value 之间相互转换。

    变量<->interface{}<->reflect.Value
  • 通过反射的方式来获取变量的值(并返回相应的类型),要求数据类型匹配,如:x 是 int,则应该使用 reflect.Value(x).Int(),否则报 panic。

  • 通过反射来修改变量的值,当使用 SetXxx 方法来完成时,需要通过对应的指针来完成,同时需要使用到 reflect.Value.Elem() 方法。

    • reflect.Value.Elem() 方法用于获取指针指向变量。

      Elem() 返回一个 v 包含的接口(interface),或者 v 指向的指针变量 (the pointer v points to)。

      第二种情况的示意图如下:

      如果传入的 v 的 Kind 不是 interface{} 或者 指针类型,Elem()报 panic。

      如果 v 还未被分配地址 (v == nil),则返回 zero Value。

      func testInt(b interface{}) {
      val := reflect.ValueOf(b)
      fmt.Printf("val type = %T\n", val)
      val.Elem().SetInt(110)
      fmt.Printf("val = %v\n", val)
      }

[Golang]Go语言入门笔记的更多相关文章

  1. golang | Go语言入门教程——结构体初始化与继承

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是golang专题第10篇文章,我们继续来看golang当中的面向对象部分. 在上一篇文章当中我们一起学习了怎么创建一个结构体,以及怎么 ...

  2. [C语言入门笔记]循环与运算符

    循环与运算符 什么是循环? 循环就是一个不停工作的东西,可以反复的实现一个功能,这个才是计算机的重点.计算机可以重复的做一件事情,这样子可以省很多事情 循环的种类有哪些? While Do while ...

  3. [C语言入门笔记]分支结构与数组

    分支结构与数组 什么是分支结构? 分支结构是用户或者程序可以选择下一步执行哪个语句 分支结构有哪些? If If Else If Else If Switch 在初学者的学习过程中第一种和第二种比较普 ...

  4. [C语言入门笔记]变量与数据类型

    变量与数据类型 什么是变量? 变量是一个变化的量 是内存中的一个空间 变量的定义方法是什么? 数据类型 变量名 = 值; 数据类型有哪些? 整型int 浮点型float double 字符型char ...

  5. golang微服务框架go-micro 入门笔记2.4 go-micro service解读

    本章节阐述go-micro 服务发现原理 go-micro架构 下图来自go-micro官方 阅读本文前你可能需要进行如下知识储备 golang分布式微服务框架go-micro 入门笔记1:搭建go- ...

  6. golang微服务框架go-micro 入门笔记2.3 micro工具之消息接收和发布

    本章节阐述micro消息订阅和发布相关内容 阅读本文前你可能需要进行如下知识储备 golang分布式微服务框架go-micro 入门笔记1:搭建go-micro环境, golang微服务框架go-mi ...

  7. golang微服务框架go-micro 入门笔记2.2 micro工具之微应用利器micro web

    micro web micro 功能非常强大,本文将详细阐述micro web 命令行的功能 阅读本文前你可能需要进行如下知识储备 golang分布式微服务框架go-micro 入门笔记1:搭建go- ...

  8. golang微服务框架go-micro 入门笔记2.1 micro工具之micro api

    micro api micro 功能非常强大,本文将详细阐述micro api 命令行的功能 重要的事情说3次 本文全部代码https://idea.techidea8.com/open/idea.s ...

  9. c语言入门这一篇就够了-学习笔记(一万字)

    内容来自慕课网,个人学习笔记.加上了mtianyan标签标记知识点. C语言入门 -> Linux C语言编程基本原理与实践 -> Linux C语言指针与内存 -> Linux C ...

随机推荐

  1. Spring boot + Vue axios 文件下载

    后端代码: @GetMapping("/{sn}") @ApiOperation(value = "获取文件",notes = "获取文件" ...

  2. golang中的接口值

    package main import ( "bytes" "fmt" "io" ) // 此处的w参数默认是一个空接口,当传递进来buf参 ...

  3. python 线程池使用

    传统多线程方案会使用"即时创建, 即时销毁"的策略.尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于 ...

  4. http8种请求方式

    根据HTTP标准,HTTP请求可以使用多种请求方法. HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法. HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELE ...

  5. MySQL基本使用(开机自启动-环境变量-忘记密码-统一编码)

    目录 一:mysql简介 1.什么是MySQL? 2.MySQL的本质 3.MySQL的特点与优势 二:基本操作命令 1.登录服务端 2.2.结束符c 3.查看当前所有的库名称 4.取消之前的命令 5 ...

  6. vi与vim编辑器与解决vim编辑异常

    目录 一:vi与vim编辑器 二:解决vim编辑异常 一:vi与vim编辑器 vim是vi的升级版编辑器,就是vim比vi丰富一些. 1.安装vim 命令 yum install vim -y 2.打 ...

  7. kubernetes集群各模块功能描述

    Master节点: Master节点上面主要由四个模块组成,APIServer,schedule,controller-manager,etcd APIServer: APIServer负责对外提供R ...

  8. python17day

    昨日回顾 自定义模块等 今日内容 自定义模块 模块是什么? 抖音:20万行代码全部放在一个py文件? 为什么不行? 代码多,读取时间长 代码不容易维护 所以应该? 一个py文件拆分100个文件,100 ...

  9. hexo博客如何插入图片

    Hexo是一个静态的博客网站生成器,生成一个博客只需要分分钟的时间就能搞定. Hexo的博文是支持Markdown格式的,发表一篇文章只需要简简单单的几个命令. hexo new '文章'就会生成一个 ...

  10. dp学习(二)

    树和图上的dp. 4. 简单树形dp 这些是最为简单的树形dp. 一般来说,树形dp是通过子树的dp值推出当前点的dp值. 在这里,我们默认当前节点为u,它的儿子节点为v,树的根为rt. 例题4.1 ...