基础

go编程时光

关键字

25
package import const var type
导入包 常量 定义变量 定义类型
map struct interface func go
map类型 定义结构体 定义接口 函数定义 并发执行
if else goto select chan
选择结构 选择结构 跳转语句 go特有的channel选择结构 定义channel
switch case break default fallthrough
选择结构 选择结构标签 跳出循环 选择结构的默认选项
switch、select
开启穿透能力
for range continue return defer
循环 从slice、map等结构中取元素 跳过本次循环 返回 延迟执行

数据类型

分类 数据类型 占用字节 比特位 最大值 用途
有符号整型 int C D E F
无符号整型 uint C D E F
单精度浮点型 float32 C D E F
单精度浮点型 float64 C D E F
ACSII字符 byte 1 8 255 表示 ACSII 表中的一个字符,和 uint8 类型本质上没有区别。不支持中文
Unicode字符 rune 3 32   表示一个 Unicode字符,和 int32 本质上也没有区别。类型本质上没有区别。支持中文
字符串 B C D E 本质是一个 byte 数组, uft-8 编码。英文字母占 1 个字节,中文字符占用 3 个字节
数组 array C D E 数组的长度是固定的,所以在Go语言中很少直接使用数组
声明是需要指定长度
切片 Slice C D E 切片是对数组的一个连续片段【左闭右开】的引用(可以容纳若干类型相同的元素的容器)
无法确定其值的长度,声明时可不指定长度
切片的第三个数,影响的只是切片的容量,而不会影响长度
字典 map C D E 由若干个 key:value 这样的键值对映射组合在一起的数据结构,它是哈希表的一个实现,key唯一
key 不能是切片,不能是字典,不能是函数
在声明字典时,必须指定好你的key和value是什么类型
布尔类型 boot C D E true 和 false
指针 pointer(ptr) C D E 内存地址
  • 为uint8 和 uint32 ,直观上让人以为这是一个数值,但是实际上,它也可以表示一个字符,所以为了消除这种直观错觉,就诞生了 byte 和 rune 这两个别名类型。
  • Unicode是一个可以表示世界范围内的绝大部分字符的编码规范
  • 一个切片具备的三个要素:类型(Type),长度(size),容量(cap)
  • 切片、指针 都是 引用类型

int 与 uint(无符号数)

  • 当你在32位的系统下,int 和 uint 都占用 4个字节,也就是32位。
  • 若你在64位的系统下,int 和 uint 都占用 8个字节,也就是64位。

    • 2进制:以 0b 或 0B 为前缀
    • 8进制:以 0o 或者 0O 为前缀
    • 16进制:以 0x 为前缀

浮点数

科学计数法: aEb = a × 10b

浮点数 占用字节 位数 符号位 指数位 尾数位 最小值 精度 最大值
float32 4 32 1 8 23 2-23 ≈ 1.19*10-7 6 math.MaxFloat32 ≈ 3.4e38
float64 8 64 1 11 52 2-52 ≈ 2.22*10-16 15 math.MaxFloat64 ≈ 3.4e308
  • 在Go语言里,浮点数的相关部分只能由10进制表示法表示
  • 精度问题:
package main

import "fmt"

var myfloat01 float32 = 100000182
var myfloat02 float32 = 100000187
var myfloat03 float32 = 10000018
var myfloat04 float32 = 10000023

func main() {
    fmt.Println("myfloat01: ", myfloat01)
    fmt.Println("myfloat02: ", myfloat02)

    //  因为 float32 只有6位精度,只保证小数点后第7位计算结果精确,第8位开始回不精确
    fmt.Println(myfloat02 == myfloat01+5)

    fmt.Println("myfloat03 = ", myfloat03)
    fmt.Println("myfloat04 = ", myfloat04)
    fmt.Println(myfloat04 == myfloat04+5)
}

//  输出结果
//  myfloat01 =  1.00000184e+08
//  myfloat02 =  1.00000184e+08
//  false

//  myfloat03 =  1.0000018e+07
//  myfloat04 =  1.0000023e+07
//  true

语法

Tip

强类型语言 true、false 和 0、1 不能直接比较

变量/常量都只能声明一次,声明多次,编译就会报错

单引号 双引号 不是等价的:单引号用来表示字符,双引号用来表示字符串 string

使用反引 `` 号包裹的字符串会忽略里面的转义,双引号不会

go支持定义一个类型字面量,也就是别名类型。

witch-case 是顺序执行的,select-case不是顺序执行的,都要写default

  • 变量声明:var
  • 变量赋值:name := "value" 等价于 var name string = "value" // 这里要一定要使用双引号,表示字符串,而在单引号表示 rune 类型的字符
  • 声明和初始化多个变量:name, age := "wangbm", 28
  • 交换变量:b, a = a, b 【相同类型的变量才能可以交换】
  • new 函数声明一个指针变量(存放 数据的地址):var ptr = &name 等价于 ptr := new(name), 给指针赋值:*ptr = "字符串",未初始化的指针,其值是 nil
  • 匿名变量,也称作占位符,或者空白标识符,用下划线表示。优点有三:

    • 不分配内存,不占用内存空间
    • 不需要你为命名无用的变量名而纠结
    • 多次声明不会有任何问题
    • 例:a, _ := GetData()

循环

//  Go是 强类型,所以要求你条件表达式必须严格返回布尔型的数据【nil 和 0 和 1 都不行】
if (age > 18 && gender == "male") {
  分支 1
//  if 里可以允许先运行一个表达式,取得变量后
} else if age := 20;age > 18 {
  fmt.Println("已经成年了")
//   else if (或 else)和 两边的花括号,必须在同一行  
} else {
  分支 else
}

switch month {
    //  case 后可以接多个多个条件,多个条件之间是 或 的关系,用逗号相隔。
    case 3, 4, 5:
        fmt.Println("春天")
    //  case 条件常量不能重复,否则 在编译时会报错: duplicate case "value" in switch
    case 6, 7, 8:
        fmt.Println("夏天")
    case 9, 10, 11:
        fmt.Println("秋天")
    case 12, 1, 2:
        fmt.Println("冬天")
    default:
        fmt.Println("输入有误...")
}

//  switch 可不接表达式, 就相当于 if - elseif - else
score := 30
switch {
    case score >= 95 && score <= 100:
        fmt.Println("优秀")
    //   case 使用关键字 fallthrough 开启穿透能力
        fallthrough
    case score >= 80:
        fmt.Println("良好")
    case score >= 60:
        fmt.Println("合格")
    case score >= 0:
        fmt.Println("不合格")
    default:
        fmt.Println("输入有误...")
}

//  1 一个表达式:a := 1  for a <= 5 {do sth}
//  2 for i := 1; i <= 5; i++ {do sth}
//  3 for-range 遍历一个可迭代的对象  for key, value := range myarr {do sth}
//  4 无限循环
for [一个表达式 |  ( init; condition; increment ) | Range表达式 | 不接表达式]
{
   statement(s);
}

//  goto 可以打破原有代码执行顺序,直接跳转到某一行执行代码
//  通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能
//  goto语句与标签之间不能有变量声明,否则编译错误
if condition {
    do sth A
    goto flag1;
    do sth B
}

flag1: 表达式;
do sth C

延迟调用 defer

//  defer 能实现将这个 xxx 函数的调用延迟到当前函数执行完后再执行
func xxx() {
    fmt.Println("B")
}

func main() {
    defer xxx()
    fmt.Println("A")
}
//  输出: A B 

//  使用 defer 只是延时调用函数,此时传递给函数里的变量,不应该受到后续程序的影响
func main() {
    name := "go"
    defer fmt.Println(name) // 输出: go

    name = "python"
    fmt.Println(name)      // 输出: python
}
//  输出: python go

//  如果 defer 后面跟的是匿名函数,情况会有所不同, defer 会取到最后的变量值
func main() {
    name := "go"
    fmt.Println(name) // 输出: go

    defer func() {
        fmt.Println(name) // 输出: python
    }()

    name = "python"
    fmt.Println(name)      // 输出: python
}

//  多个defer 反序调用,有点类似栈一样,后进先出。
func main() {
    name := "go"
    defer fmt.Println(name) // 输出: go

    name = "python"
    defer fmt.Println(name) // 输出: python

    name = "java"
    fmt.Println(name)
}
//  输出 java python go

//  defer 是return 后才调用, 用途:当一个函数里有多个 return 时,你得多调用好多次这个函数,代码就臃肿起来了。
import "fmt"

var name string = "go"

func myfunc() string {
    defer func() {
        name = "python"
    }()

    fmt.Printf("myfunc 函数里的name:%s\n", name)
    return name
}

func main() {
    myname := myfunc()
    fmt.Printf("main 函数里的name: %s\n", name)
    fmt.Println("main 函数里的myname: ", myname)
}
//  输出如下
//  myfunc 函数里的name:go
//  main 函数里的name: python
//  main 函数里的myname:  go

信道/通道 select-case

跟 switch-case 相比,select-case 用法比较单一,它仅能用于 信道/通道 的相关操作。

package main

import (
    "fmt"
)

func main() {
    c1 := make(chan string, 1)
    c2 := make(chan string, 1)

    c2 <- "hello"

    //  在运行 select 时,会遍历所有(如果有机会的话)的 case 表达式,只要有一个信道有接收到数据,那么 select 就结束,所以输出如下
    select {
    case msg1 := <-c1:
      fmt.Println("c1 received: ", msg1)
    case msg2 := <-c2:
      fmt.Println("c2 received: ", msg2)
    //  不写default 可能导致死锁
    default:
      fmt.Println("No data received.")
    }
}

select VS switch

  • select 只能用于 channel 的操作(写入/读出/关闭),而 switch 则更通用一些;
  • select 的 case 是随机的,而 switch 里的 case 是顺序执行;
  • select 要注意避免出现死锁,同时也可以自行实现超时机制;
  • select 里没有类似 switch 里的 fallthrough 的用法;
  • select 不能像 switch 一样接函数或其他表达式。

异常机制:panic 和 recover

panic:抛出异常,使程序崩溃

  • 手动触发宕机,只需要调用 panic 这个内置函数即可 ```go package main

func main() { panic("crash") }


### recover:捕获异常,恢复程序或做收尾工作

recover 它可以让程序在发生宕机后起生回生。必须在 defer 函数中才能生效,其他作用域下,它是不工作的。

* 子协程里触发 panic,只能触发自己协程内的 defer,而不能调用 main 协程里的 defer 函数的

```go
import "fmt"

func set_data(x int) {
    defer func() {
        // recover() 可以将捕获到的panic信息打印
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()

    // 故意制造数组越界,触发 panic
    var arr [10]int
    arr[x] = 88
}

func main() {
    set_data(20)

    // 如果能执行到这句,说明panic被捕获了
    // 后续的程序能继续运行
    fmt.Println("everything is ok")
}

语句块

  • Go 使用的是词法作用域,而词法作用域依赖于语句块
  • 语句块内部声明的名字是无法被外部块访问的 —— 作用域
  • 显式语句块是由花括弧 {} 所包含的一系列语句。
  • 隐式语句块:

    • 主语句块:包括所有源码,对应内置作用域
    • 包语句块:包括该包中所有的源码(一个包可能会包括一个目录下的多个文件),对应包级作用域
    • 文件语句块:包括该文件中的所有源码,对应文件级作用域
    • for 、if、switch等语句本身也在它自身的隐式语句块中,对应局部作用域