goLang变量 | 青训营笔记

150 阅读14分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

前期准备

  • 项目初始化:go mod init 项目名
  • 开启go_modules:go env -w GO111MODULE=on go env -w GO111MODULE=auto
  • 设置代理:go env -w GOPROXY=goproxy.cn,direct
  • 下载依赖:go get 依赖

基本知识

变量,方法,常量等首字母大写,则为public类型

变量:

//变量命名;变量在声明之后,系统会自动将变量值初始化为对应类型的零值,比如 v1 的值为 0,v2 的值空字符串
var name string 
var age int
//也可以将若干个需要声明的变量放置在一起
var (
    v1 int 
    v2 string*
)

//变量初始化
var v1 int = 10   // 方式一,常规的初始化操作
var v2 = 10       // 方式二,此时变量类型会被编译器自动推导出来
v3 := 10          // 方式三,可以省略var 编译器可以自动推导出v3的类型;出现在 := 运算符左侧的变量应该是未声明过的,否则会导致编译错误

//数据类型
var v1 int            // 整型
var v2 string         // 字符串
var v3 bool           // 布尔型
var v4 [10]int        // 数组,数组元素类型为整型
var v5 struct {       // 结构体,成员变量 f 的类型为64位浮点型
    f float64
} 
var v6 *int           // 指针,指向整型
var v7 map[string]int   // map(字典),key为字符串类型,value为整型
var v8 func(a int) int  // 函数,参数类型为整型,返回值类型为整型

Go 是强类型语言,变量类型一旦确定,就不能将其他类型的值赋值给该变量,因此,布尔类型不能接受其他类型的赋值,也不支持自动或强制的类型转换;此外,由于强类型的缘故,Go 语言在进行布尔值真假判断时,对值的类型有严格限制,不同类型的值不能使用 == 或 != 运算符进行比较,在编译期就会报错

多重赋值功能

//交换 i 和 j 变量
i, j = j, i

匿名变量

//通过下划线 `_` 声明
func GetName() (userName, nickName string) { 
    return "nonfu", "学院君"
}
//若只想获得 nickName,则函数调用语句可以用如下方式实现
_, nickName := GetName()

尽管变量的标识符必须是唯一的,但你可以在某个代码块的内层代码块中使用相同名称的变量,此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放),你任何的操作都只会影响内部代码块的局部变量

常量

在 Go 语言中,常量是指编译期间就已知且不可改变的值,常量只可以是数值类型(包括整型、 浮点型和复数类型)、布尔类型、字符串类型等标量类型;通过 const 关键字定义常量时,可以指定常量类型,也可以省略(底层会自动推导)

const Pi float64 = 3.14159265358979323846 
const zero = 0.0 // 无类型浮点常量 
const (          // 通过一个 const 关键字定义多个常量,和 var 类似
    size int64 = 1024
    eof = -1  // 无类型整型常量 
) 
const u, v float32 = 0, 3  // u = 0.0, v = 3.0,常量的多重赋值 
const a, b, c = 3, 4, "foo" // a = 3, b = 4, c = "foo", 无类型整型和字符串常量

由于常量的赋值是一个编译期行为,所以右值不能出现任何需要运行期才能得出结果的表达式(包括调用函数,方法)

预定义常量

Go 语言预定义了这些常量:truefalse 和 iota

iota 比较特殊,可以被认为是一个可被编译器修改的常量,在每一个 const 关键字出现时被重置为 0,然后在下一个 const 出现之前,每出现一次 iota,其所代表的数字会自动增 1。

package main

const (    // iota 被重置为 0
    c0 = iota   // c0 = 0
    c1 = iota   // c1 = 1
    c2 = iota   // c2 = 2
)

const (
    u = iota * 2;  // u = 0
    v = iota * 2;  // v = 2
    w = iota * 2;  // w = 4
)

const x = iota;  // x = 0
const y = iota;  // y = 0

如果两个 const 的赋值语句的表达式是一样的,那么还可以省略后一个赋值表达式。因此,上面的前两个 const 语句可简写为:

const ( 
    c0 = iota 
    c1 
    c2 
)

const ( 
    u = iota * 2 
    v 
    w 
)

复合类型

  • 指针(pointer)
  • 数组(array)
  • 切片(slice)
  • 字典(map)
  • 通道(chan)
  • 结构体(struct)
  • 接口(interface)

运算符

  • 支持i++ i-- 但不支持--i ++i

  • 支持 +=-=*=/=%= 这种快捷写法

  • 由于Go 是强类型语言,只有同类型的值才能放在一起运算

  • 各种类型的整型变量都可以直接与字面常量进行比较,如intValue1 == 8

  • 位运算符

    运算符含义结果
    x & y按位与把 x 和 y 都为 1 的位设为 1
    `x \y`按位或
    x ^ y按位异或把 x 和 y 一个为 1 一个为 0 的位设为 1
    ^x按位取反把 x 中为 0 的位设为 1,为 1 的位设为 0
    x << y左移把 x 中的位向左移动 y 次,每次移动相当于乘以 2
    x >> y右移把 x 中的位向右移动 y 次,每次移动相当于除以 2
  • 逻辑运算符

    运算符含义结果
    x && y逻辑与运算符(AND)如果 x 和 y 都是 true,则结果为 true,否则结果为 false
    `x \y`逻辑或运算符(OR)
    !x逻辑非运算符(NOT)如果 x 为 true,则结果为 false,否则结果为 true
  • 运算符优先级

    6      ^(按位取反) !
    5      *  /  %  <<  >>  &  &^
    4      +  -  |  ^(按位异或)
    3      ==  !=  <  <=  >  >=
    2      &&
    1      ||
    

浮点数

Go 语言中的浮点数采用IEEE-754 标准的表达方式,定义了两个类型:float32 和 float64,其中 float32 是单精度浮点数,可以精确到小数点后 7 位(类似 PHP、Java 等语言的 float 类型),float64 是双精度浮点数,可以精确到小数点后 15 位(类似 PHP、Java 等语言的 double 类型)。

在实际开发中,应该尽可能地使用 float64 类型,因为 math 包中所有有关数学运算的函数都会要求接收这个类型。

字符串

var str string         // 声明字符串变量
str = "Hello World"    // 变量初始化
str2 := "你好,学院君"   // 也可以同时进行声明和初始化

在 Go 语言中,字符串是一种基本类型,默认是通过 UTF-8 编码的字符序列,当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节,比如中文编码通常需要 3 个字节。

  • 格式化输出 fmt.Printf("The length of "%s" is %d \n", str, len(str))

  • 字符串操作:连接,切片,遍历

  • 多行字符串

    //使用 ` 
    var str string         // 声明字符串变量
    str = "Hello World"    // 变量初始化
    str2 := "你好,学院君"   // 也可以同时进行声明和初始化
    //或者使用 +
    results := "Search results for "Golang":\n" +
    "- Go\n" +
    "- Golang\n" +
    "- Golang Programming\n"
    
    //输出结果
    Search results for "Golang":
    - Go
    - Golang
    - Golang Programming
    

字符串是一种不可变值类型,一旦初始化之后,它的内容不能被修改

转义字符

  • \n :换行符
  • \r :回车符
  • \t :tab 键
  • \u 或 \U :Unicode 字符
  • \ :反斜杠或自身

基本数据类型之间的转化

数组

  • 数组的定义

    var a [8]byte // 长度为8的数组,每个元素为一个字节
    var b [3][3]int // 二维数组(9宫格)
    var c [3][3][3]float64 // 三维数组(立体的9宫格)
    var d = [3]int{1, 2, 3}  // 声明时初始化
    var e = new([3]string)   // 通过 new 初始化
    
    //此外,还可以通过这种语法糖省略数组长度的声明,这种情况下,Go 会在编译期自动计算出数组长度。
    a := [...]int{1, 2, 3}
    
    //数组在初始化的时候,如果没有填满,则空位会通过对应的元素类型零值填充
    
  • 数组的遍历

    • 使用for

    • Go 语言还提供了一个关键字 range,用于以更优雅的方式遍历数组中的元素,range 表达式返回两个值,第一个是数组下标索引值,第二个是索引对应数组元素值

      for i, v := range arr { 
          fmt.Println("Element", i, "of arr is", v) 
      }
      //如果我们不想获取索引值
      for _, v := range arr {
         // ...
      }
      //如果只想获取索引值
      for i := range arr {
      // ...
      }
      

切片(golang中新的数据类型)

  • 在 Go 语言中,切片是一个新的数据类型,与数组最大的不同在于,切片的类型字面量中只有元素的类型,没有长度, 切片的长度可以随着元素数量的增长而增长(但不会随着元素数量的减少而减少)

    var slice []string = []string{"a", "b", "c"}    
    
  • 基于数组创建

    (切片可以只使用数组的一部分元素或者整个数组来创建,甚至可以创建一个比所基于的数组还要大的切片)

    // 先定义一个数组
    months := [...]string{"January", "February", "March", "April", "May", 
              "June", "July", "August", "September", "October", "November", "December"}
    
    // 基于数组创建切片
    q2 := months[3:6]    // 第二季度
    summer := months[5:8]  // 夏季
    
    fmt.Println(q2)
    fmt.Println(summer)  
    

    和字符串切片一样,这也是个左闭右开 [ , )的集合

  • 基于 months 的所有元素创建切片(全年)

  • all := months[:]

  • 基于 months 的前 6 个元素创建切片(上半年)

  • firsthalf := months[:6]

  • 基于从第 6 个元素开始的后续元素创建切片(下半年)

  • secondhalf := months[6:]

  • 基于切片

  • q1 := firsthalf[:3]` // 基于 firsthalf 的前 3 个元素构建新切片

  • 直接创建

    Go: 语言提供的内置函数 make() 可以用于灵活地创建切片

    //要创建一个初始长度为 5 的整型切片,可以这么做:
    mySlice1 := make([]int, 5)
    
    //要创建一个初始长度为 5、容量为 10 的整型切片,可以这么做(通过第三个参数设置容量):
    mySlice2 := make([]int, 5, 10)
    
    //此外,还可以直接创建并初始化包含 5 个元素的数组切片(长度和容量均为5):
    mySlice3 := []int{1, 2, 3, 4, 5}
    
    //和数组一样,所有未初始化的切片,会填充元素类型对应的零值。
    
  • 动态增加元素

    切片比数组更强大之处在于支持动态增加元素,甚至可以在容量不足的情况下自动扩容

    通常一个切片的长度值小于等于其容量值,我们可以通过 Go 语言内置的 cap() 函数和 len() 函数来获取某个切片的容量和实际长度

    可以通过 append() 函数向切片追加新元素

    var oldSlice = make([]int, 5, 10)
    newSlice := append(oldSlice, 1, 2, 3)
    
    //函数 append() 的第二个参数是一个不定参数,我们可以按自己需求添加若干个元素(大于等于 1 个)
    //可以直接将一个切片追加到另一个切片的末尾
    appendSlice := []int{1, 2, 3, 4, 5}
    newSlice := append(oldSlice, appendSlice...)  // 注意末尾的 ... 不能省略
    
  • 内容复制

    切片类型还支持 Go 语言的另一个内置函数 copy(),用于将元素从一个切片复制到另一个切片。如果两个切片不一样大,就会按其中较小的那个切片的元素个数进行复制。

    slice1 := []int{1, 2, 3, 4, 5} 
    slice2 := []int{5, 4, 3}
    
    // 复制 slice1 到 slice 2
    copy(slice2, slice1) // 只会复制 slice1 的前3个元素到 slice2 中
    // slice2 结果: [1, 2, 3]
    
    // 复制 slice2 到 slice 1
    copy(slice1, slice2) // 只会复制 slice2 的 3 个元素到 slice1 的前 3 个位置
    // slice1 结果:[5, 4, 3, 4, 5]
    
  • 动态删除元素

    其实是通过切片的切片实现的「伪删除」,也可以用append()和copy()实现

    slice3 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    slice3 = slice3[:len(slice3) - 5]  // 删除 slice3 尾部 5 个元素
    slice3 = slice3[5:]  // 删除 slice3 头部 5 个元素
    
    //输出结果
    slice1: [0 2 0 0 0]
    slice2: [0 6]
    
  • 数据共享

    切片底层是基于数组实现的;若基于 slice1 创建 slice2 ,则它们的数组指针指向了同一个数组,因此,修改 slice2 元素会同步到 slice1,因为修改的是同一份内存数据,这就是数据共享问题

    解决数据共享问题

    slice1 := make([]int, 4)
    slice2 := slice1[1:3]
    slice1 = append(slice1, 0)
    slice1[1] = 2
    slice2[1] = 6
    
    fmt.Println("slice1:", slice1)
    fmt.Println("slice2:", slice2)
    
    //输出结果
    slice1: [0 2 0 0 0]
    slice2: [0 6]
    

    虽然 slice2 是基于 slice1 创建的,但是修改 slice2 不会再同步到 slice1,因为 append 函数会重新分配新的内存,然后将结果赋值给 slice1,这样一来,slice2 会和老的 slice1 共享同一个底层数组内存,不再和新的 slice1 共享内存,也就不存在数据共享问题了。

    但是这里有个需要注意的地方,就是一定要重新分配内存空间,如果没有重新分配,依然存在数据共享问题:

    slice1 := make([]int, 4, 5) //!设定容量为5
    slice2 := slice1[1:3]
    slice1 = append(slice1, 0)
    slice1[1] = 2
    slice2[1] = 6
    
    fmt.Println("slice1:", slice1)
    fmt.Println("slice2:", slice2)
    
    //输出结果
    slice1: [0 2 6 0 0]
    slice2: [2 6]
    

字典

所谓字典,其实就是存储键值对映射关系的集合,和 Redis 一样,Go 字典也是个无序集合,底层不会按照元素添加顺序维护元素的存储顺序。

字典声明

  • 字典的声明基本上没有多余的元素,比如:
//先声明再初始化
var testMap map[string]int
//声明和初始化合并
testMap := map[string]int{
  "one": 1,
  "two": 2,
  "three": 3,
}

//前面我们提到 Go 字典是个无序集合,所以如果我们通过 fmt.Println(testMap) 打印 testMap 的值,得到的可能是下面这样的结果:

map[one:1 three:3 two:2]

其中,testMap 是声明的字典变量名,string 是键的类型,int 则是其中所存放的值类型。

  • 此外,还可以像切片那样,通过 Go 语言内置的函数 make() 来初始化一个新字典:

    var testMap = make(map[string]int)

    通过这种方式初始化后,可以往字典中添加键值对(前面那种声明方式不能这么操作,否则编译期间会抛出 panic):

    testMap["one"] = 1

    testMap["two"] = 2

    testMap["three"] = 3

  • 还可以通过 make 函数的第二个参数选择是否在创建时指定该字典的初始存储容量(超出会自动扩容):

testMap = make(map[string]int, 100)

查找字典元素

value, ok := testMap["one"] 
if ok { // 找到了
  // 处理找到的value 
}

从字典中查找指定键时,会返回两个值第一个是真正返回的键值,第二个是是否找到的标识判断是否在字典中成功找到指定的键,不需要检查取到的值是否为 nil只需查看第二个返回值ok,这是一个布尔值,如果查找成功,返回 true,否则返回 false,配合 := 操作符,让你的代码没有多余成分,看起来非常清晰易懂。

在声明字典的键类型时要求数据类型必须是支持通过 == 或 != 进行判等操作的类型,比如数字类型、字符串类型、数组类型、结构体类型等

删除字典元素

delete(testMap, "four")

指针

a := 100
//方式一
var ptr *int  // 声明指针类型
ptr = &a      // 初始化指针类型值为变量 a 
//方式二
ptr := &a
//方式三
ptr := new(int)
*ptr = 100

fmt.Printf("%p\n", ptr)
fmt.Printf("%d\n", *ptr)

//输出结果:
0xc0000a2000
100

上面代码中的 ptr 就是一个指针类型,表示指向存储 int 类型值的指针。ptr 本身是一个内存地址值,当指针被声明后,没有指向任何变量内存地址时,它的零值是 nil,所以需要通过内存地址进行赋值(通过 &a 可以获取变量 a 所在的内存地址),赋值之后,可以通过 *ptr 获取指针指向内存地址存储的变量值(我们通常将这种引用称作「间接引用」)

使用场景

指针在 Go 语言中有两个典型的使用场景:

  • 类型指针
  • 切片

作为类型指针时,允许对这个指针类型数据指向的内存地址存储值进行修改,传递数据时如果使用指针则无须拷贝数据从而节省内存空间,此外和 C 语言中的指针不同,Go 语言中的类型指针不能进行偏移和运算,因此更为安全。

切片类型我们前面已经介绍过,由指向数组起始元素的指针、元素数量和容量组成,所以切片与数组不同,是引用类型,而非值类型。

unsafe.Pointer

我们前面介绍的指针都是被声明为指定类型的,而 unsafe.Pointer 是特别定义的一种指针类型,它可以包含任意类型变量的地址(类似 C 语言中的 void 类型指针)。Go 官方文档对这个类型有如下四个描述:

  1. 任何类型的指针都可以被转化为 unsafe.Pointer
  2. unsafe.Pointer 可以被转化为任何类型的指针;
  3. uintptr 可以被转化为 unsafe.Pointer
  4. unsafe.Pointer 可以被转化为 uintptr