三、复合类型

1.无类型常量

一个常量可以有任意一个确定的基础类型,例如int或float64,或者是类似time.Duration这样命名的基础类型,但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算;你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。

通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。例如,例子中的ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围,但是它们依然是合法的常量,而且像下面的常量表达式依然有效(译注:YiB/ZiB是在编译期计算出来的,并且结果常量是1024,是Go语言int变量能有效表示的):

1
fmt.Println(YiB/ZiB) // "1024"

另一个例子,math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方:

1
var x float32 = math.Pi
2
var y float64 = math.Pi
3
var z complex128 = math.Pi

如果math.Pi被确定为特定类型,比如float64,那么结果精度可能会不一样,同时对于需要float32或complex128类型值的地方则会强制需要一个明确的类型转换:

1
const Pi64 float64 = math.Pi
2
3
var x float32 = float32(Pi64)
4
var y float64 = Pi64
5
var z complex128 = complex128(Pi64)

对于常量面值,不同的写法可能会对应不同的类型。例如0、0.0、0i和\u0000虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true和false也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。

前面说过除法运算符/会根据操作数的类型生成对应类型的结果。因此,不同写法的常量除法表达式可能对应不同的结果:

1
var f float64 = 212
2
fmt.Println((f - 32) * 5 / 9)     // "100"; (f - 32) * 5 is a float64
3
fmt.Println(5 / 9 * (f - 32))     // "0";   5/9 is an untyped integer, 0
4
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float

只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,就像下面的第一行语句,或者出现在有明确类型的变量声明的右边,如下面的其余三行语句,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。

1
var f float64 = 3 + 0i // untyped complex -> float64
2
f = 2                  // untyped integer -> float64
3
f = 1e123              // untyped floating-point -> float64
4
f = 'a'                // untyped rune -> float64

上面的语句相当于:

1
var f float64 = float64(3 + 0i)
2
f = float64(2)
3
f = float64(1e123)
4
f = float64('a')

无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。对于浮点数和复数,可能会有舍入处理:

1
const (
2
	deadbeef = 0xdeadbeef // untyped int with value 3735928559
3
	a = uint32(deadbeef)  // uint32 with value 3735928559
4
	b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
5
	c = float64(deadbeef) // float64 with value 3735928559 (exact)
6
	d = int32(deadbeef)   // compile error: constant overflows int32
7
	e = float64(1e309)    // compile error: constant overflows float64
8
	f = uint(-1)          // compile error: constant underflows uint
9
)

对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,就像下面的例子:

1
i := 0      // untyped integer;        implicit int(0)
2
r := '\000' // untyped rune;           implicit rune('\000')
3
f := 0.0    // untyped floating-point; implicit float64(0.0)
4
c := 0i     // untyped complex;        implicit complex128(0i)

注意有一点不同:无类型整数常量转换为int,它的内存大小是不确定的,但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128。
如果不知道浮点数类型的内存大小是很难写出正确的数值算法的,因此Go语言不存在整型类似的不确定内存大小的浮点数和复数类型。

如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型,像下面例子这样:

1
var i = int8(0)
2
var i int8 = 0

当尝试将这些无类型的常量转为一个接口值时,这些默认类型将显得尤为重要,因为要靠它们明确接口对应的动态类型。

1
fmt.Printf("%T\n", 0)      // "int"
2
fmt.Printf("%T\n", 0.0)    // "float64"
3
fmt.Printf("%T\n", 0i)     // "complex128"
4
fmt.Printf("%T\n", '\000') // "int32" (rune)

四、引用类型

1.切片
  • 介绍

    在初始定义数组是, 我们并不知道需要多大的数组,因此我们就需要“动态数组”, 在Go里面这种数据结构叫做slice

    切片解决了数组长度不能扩展, 以及基本类型数据传递是产生副本的问题

  • 切片创建

    • 常用创建方式

      1
      var s1 []int
      2
      s2 := []byte {'a', 'v', 'c'}
      3
      fmt.Println(s1)
      4
      fmt.Println(s2)
    • 从数组创建

      slice 可以从一个数组中再次声明, slice通过array[i:j]来获取,其中i是数组的开始位置, j是结束位置,但不包括arrar[j], 它的长度是 j-i

      1
      var s1 []int
      2
      s2 := []byte {'a', 'v', 'c'}
      3
      fmt.Println(s1)
      4
      fmt.Println(s2)
      5
      6
      // 声明一个含有10个元素类型为byte的数组
      7
      var arr = [10]byte {'a','c','d','f','m', 'r','s','t','x'}
      8
      9
      // 声明两个含有byte的slice
      10
      var a, b []byte
      11
      12
      //  a指向数组的第3个元素开始,并到第五个元素结束,现在a含有的元素: ar[2]、ar[3]和ar[4]
      13
      a = arr[2: 5]
      14
      // b是数组arr的另一个slicre,b的元素是:ar[3]和ar[4]
      15
      b = arr[3, 5]

      注意:声明数组时,方括号内写明了数组的长度或使用…自动计算长度, 而声明slice时, 方括号内没有任何字符。

    • 从切片创建

      1
      oldSlice := []int{1, 2, 3}
      2
      newSlice := oldSlice[:6] //基于切片前6个元素创建,没有的默认0
      3
      4
      如果选择的旧切片长度超出了旧切片的cap()值(切片存储长度),则不合法。
    • 使用make函数创建

      1
      slice1 := make([]int, 5) 	//创建初始值为0,个数为5的切片
      2
      slice2 := make([]int, 5, 10)  //创建初始值为10,个数为5的切片
      3
      slice3 := []int{1, 2, 3, 4} //创建并初始化
      4
      fmt.Println(slice1)
      5
      fmt.Println(slice2)
      6
      fmt.Println(slice3)
      7
      8
      GOROOT=C:\Go #gosetup
      9
      GOPATH=C:\Go\bin #gosetup
      10
      C:\Go\bin\go.exe build -o C:\Users\dell\AppData\Local\Temp\___go_build_go_slice_go.exe D:/Project/Go_project/go_basic_project/go_slice.go #gosetup
      11
      C:\Users\dell\AppData\Local\Temp\___go_build_go_slice_go.exe #gosetup
      12
      [0 0 0 0 0]
      13
      [0 0 0 0 0]
      14
      [1 2 3 4]
  • 切片操作

    • 切片常见内置函数

      • len() 返回切片长度
      • cap() 返回切片底层数组容量
      • append() 对切片追加元素
      • copy() 复制一个切片

      切片空间与元素个数

      1
      slice1 := make([]int, 5, 10)
      2
      fmt.Println(len(slice1)) 	// 5
      3
      fmt.Println(cap(slice1))	// 10
      4
      fmt.Println(slice1)			// [0 0 0 0 0]

      切片操作

      1
      //切片增加
      2
      slice1 = append(slice1,1,2)
      3
      fmt.Println(slice1)						//输出[0 0 0 0 0 1 2]
      4
      5
      //切片增加一个新切片
      6
      sliceTemp := make([]int,3)
      7
      slice1 = append(slice1,sliceTemp...)
      8
      fmt.Println(slice1)						//输出[0 0 0 0 0 1 2 0 0 0]
      9
      10
      //切片拷贝
      11
      s1 := []int{1,3,6,9}
      12
      s2 := make([]int, 10)	//必须给与充足的空间
      13
      num := copy(s2, s1)
      14
      15
      fmt.Println(s1)			//[1 3 6 9]
      16
      fmt.Println(s2)			//[1 3 6 9 0 0 0 0 0 0]
      17
      fmt.Println(num)		//4
      18
      19
      //切片中删除元素
      20
      s1 := []int{1,3,6,9}
      21
      index := 2					//删除该位置元素
      22
      s1 = append(s1[:index], s1[index+1:]...)
      23
      fmt.Println(s1)				//[1 3 9]
      24
      25
      // 切片拷贝
      26
      s1 := []int{1,2,3,4,5}
      27
      s2 := []int{1,2,3}
      28
      copy(s1,s2) 				//复制s1前三个元素到slice2
      29
      copy(s2,s1)	 				//复制s2的个元素到slice1前3位置

      注意: 没有...会编译错误,默认第二个参数后是元素值, 传入切片需要展开,如果追加长度超过当前已分配的存储空间, 切片会自动分配更大的内存

    • 切片的一些简便操作

      • slice的默认开始位置是0,ar[:n]等价于ar[0:n]

      • slice的第二个序列默认是数组的长度,ar[n:]等价于ar[n:len(ar)]

      • 如果从一个数组里面直接获取slice,可以这样ar[:],因为默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)]

      • 切片的遍历可以使用for循环,也可以使用range函数

        1
        // 声明一个数组
        2
        var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
        3
        // 声明两个slice
        4
        var aSlice, bSlice []byte
        5
        6
        // 演示一些简便操作
        7
        aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c
        8
        aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j
        9
        aSlice = array[:] // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素
        10
        11
        // 从slice中获取slice
        12
        aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7
        13
        bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f
        14
        bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f
        15
        bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h
        16
        bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g
  • 切片的截取

    • s[n]:切片s中索引为位置为n的项
    • s[:]:从切片s的索引位置0到len(s)-1所获得的切片
    • s[low:]:从切片s的索引位置low到len(s)-1所获得的切片
    • s[:high]:从切片s的索引位置0到high所获得的切片
    • s[low:high]:从切片s的索引位置low到high所获得的切片
    • s[low:high:max]:从low到high的切片,且容量cap=max-low
  • 字符串转切片

    1
    str := "hello,世界"
    2
    a := []byte(str)		//字符串转换为[]byte类型切片
    3
    b := []rune(str)		//字符串转换为[]rune类型切片
  • 切片存储结构

    与数组相比, 切片多了一个存储能力值的概念,即元素个数与分配空间可以是两个不同的值,其结构如下所示:

    1
    type slice struct {
    2
    	arrary = unsafe.Pointer		//指向底层数组的指针
    3
    	len int						//切片元素数量
    4
    	cap int						//底层数组的容量
    5
    }

    所以切片会通过内部的指针和相关属性引用数据片段,实现了变长方案,slice并不是真正意义上的动态数组

    合理设置存储能力, 可以大幅提升性能, 比如纸袋最多元素个数为50, 那么提前设置为50, 而不是先设为30, 可以明显减少重新分配内存的操作

2.集合map

​ 需要是用任意类型的关联,就需要使用到集合, 比如学号,名字。go语言提供了映射关系的容器是map,内部使用散列表实现

​ 大多数语言中映射关系容器使用两种算法: 散列表和平衡树

​ 散列表可以简单的描述为一个数组, 数组的每个元素都是列表, 根据散列函数获得每个元素的特征值,将特征值作为映射的键,如果特征值重复,表示元素发生了碰撞,需要尽量避免碰撞,这样就需要多容器扩容,每次扩容,耶稣都需要重新放入, 较为耗时

  • map的创建

    go内置了map类型, map是一个无序键值对集合(或称字典)

    • 普通创建:

      1
      //声明一个map类型,[]内的类型指任意可以进行比较的类型 int指值类型
      2
      m := map[string]int{"a":1,"b":2}
      3
      fmt.Print(m["a"])
    • make方式创建map:

      1
      type Person struct{
      2
      	ID string
      3
      	Name string
      4
      }
      5
      6
      func main() {
      7
      	var m map[string] Person				
      8
              m = make(map[string] Person)
      9
      	m["123"] = Person{"123","ykyk"}
      10
      	p,isFind := m["123"]
      11
      	fmt.Println(isFind)		//true
      12
      	fmt.Println(p)			//{123 ykyk}
      13
      }

      key的类型,key可以有很多类型,比如bool, 数组,string, channel, 还可以是只包含前面几个类型的接口, 结构体, 数组

      通常key为int,string

      注意:slice, map还有function不可以, 因为他们不能使用 ==来判断

    • 使用

      map类型的读取和设置也和slice一样,通过key来操作,只是slice的index只能是int类型,而map多了很多类型, 可以是int,可以是string及所有完全定义了 ==!=操作的类型

      1
      // 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化
      2
      var numbers map[string]int
      3
      // 另一种map的声明方式
      4
      numbers = make(map[string]int)
      5
      numbers["one"] = 1 //赋值
      6
      numbers["ten"] = 10 //赋值
      7
      numbers["three"] = 3
      8
      9
      fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据
      10
      // 打印出来如:第三个数字是: 3

      map的遍历同数组一样, 使用for-range的结构遍历

      注意:

      ​ map是无序的,每次打印出来的map都会不一样, 不能通过index获取,必须通过key来获取

      ​ map的长度是不固定的,也就是和slice一样,也是一种引用类型

      ​ 内置的len函数同样适用于map,返回map拥有的key的数量

      ​ map的值可以很方便的修改,通过numbers["one"]=11可以很容易的把key为one的字段值修改为11

      ​ map和其他基本类型不同,不是thread-safe(线程安全), 在多个go-routine存取时, 必须使用mutex lock机制

  • 删除元素

    1
    // 初始化一个字典
    2
    rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
    3
    // map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true
    4
    csharpRating, ok := rating["C#"]
    5
    if ok {
    6
        fmt.Println("C# is in the map and its rating is ", csharpRating)
    7
    } else {
    8
        fmt.Println("We have no rating associated with C# in the map")
    9
    }
    10
    11
    delete(rating, "C") // 删除key为C的元素

    注意:go没有提供清空元素的方法,可以重新马克一个新的map,不用担心垃圾回收的效率,因为go中并行的垃圾回收效率比写一个清空函数高效很多

  • sync.Map

    Go内置的map只读是线程安全的,读写是线程不安全的,并发安全的map可以使用标准包sync中的map

    演示并发洗map的问题:

    1
    package main
    2
    3
    func main() {
    4
    5
    	m := make(map[int]int)
    6
    7
    	go func() {			
    8
    		for {				//无限写入
    9
    			m[1] = 1
    10
    		}
    11
    	}()
    12
    13
    	go func() {
    14
    		for {				//无限读取
    15
    			_ = m[1]
    16
    		}
    17
    	}()
    18
    19
    	for {}					//无限循环,让并发程序在后台执行
    20
    21
    }

    错误提示:fatal error: concurrent map read and map write,即出现了并发读写,因为用两个并发程序不断的对map进行读和写,产生了竞态问题。map背部会对这种错误进行检查并提前发现。

    需要并发读写时,一般都是加锁,但是这样做性能不高在go1.9版本中提供了更高效并发安全的sync.Map。

    sync.Map的特点:

    • 无序初始化,直接声明即可
    • sync.Map不能私用map的方式进行取值和设置操作,而是使用sync.Map的方法进行调用,Store表示存储,Load表示调用,Delete表示删除
    • 使用Range配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,需要继续迭代时,返回true,终止迭代返回false
    1
    package main
    2
    3
    import (
    4
    	"fmt"
    5
    	"sync"
    6
    )
    7
    8
    func main() {
    9
    10
    	var scene sync.Map
    11
    12
    	//保存键值对
    13
    	scene.Store("id",2)
    14
    	scene.Store("name","ykyk")
    15
    16
    	//根据键取值
    17
    	fmt.Println(scene.Load("name"))			
    18
    19
    	//遍历	
    20
    	scene.Range(func(k, v interface{}) bool{
    21
    		fmt.Println(k,v)
    22
    		return true
    23
    	})
    24
    }

    注意:map没有提供获取map数量的方法,可以在遍历时手动计算。sync.Map为了并发安全。损失了一定的性能。

3.指针
  • 指针的创建

    示例:

    1
    v := "3"
    2
    ptr := &v
    3
    value := *ptr
    4
    fmt.Printf("指针地址为:%p\n", ptr)					// 输出0x.....16进制数
    5
    fmt.Printf("指针地址内存储的值为:%s\n", value)		// 输出3

    指针类型的声明方式

    • 方式一

      1
      var a int = 10
      2
      var p *int = &a	     //声明指针类型
      3
      fmt.Println(p)      //输出 0xc.....16进制数
      4
      fmt.Println(*p)    // 10
    • 方式二

      1
      var p *int
      2
      p = new(int)   //申请一个int类型的地址空间
      3
      *p = 666      //存储地址内内容为666
      4
      fmt.Println(p)
    • 注意:

      • go同样支持多级指针, 如**t
      • 空指针:生命但未初始化的指针
      • 野指针: 引用了无效地址的指针
      • go不支持->运算符指针运算, 可以直接使用.访问目标成员
    • 声明方式三

      1
      str := new(string) // 申请一个string类型的指针内存
      2
      *str = "hello"
      3
      fmt.Println(&str)
  • 指针实现变量值交换

    1
    func swap (p1,p2 *int) {
    2
       *p1,*p2 = *p2,*p1
    3
    }
  • 结构体指针

    1
    type User struct{
    2
    	name string
    3
    	age int
    4
    }
    5
    6
    func main() {
    7
    	var u = User{
    8
    		name:"lisi",
    9
    		age: 18,
    10
    	}
    11
    	p := &u
    12
    	fmt.Println(u.name)		//输出李四
    13
    	fmt.Println(p.name)		//输出李四
    14
    }
  • go不支持指针运算

    由于垃圾回收机制存在, 指针运算造成很多困扰, 所以go直接禁止了指针运算

    1
    a := 1
    2
    p := &a
    3
    p++        //报错:non-numeric type *int
  • 变量声明周期与栈逃逸机制

    函数中允许返回局部变量的地址,Go编译器使用栈逃逸机制将这种局部变量分配在堆上:

    1
    var p = f()
    2
    func f() *int {
    3
    	v := 1
    4
    	return &v			// 返回函数中的局部变量地址是安全的,因为p仍然在引用他
    5
    }

    变量的生命周期指在程序运行期间变量有效存在的时间段:

    • 包级别声明的变量,其生命周期和整个程序的运行周期是一致的
    • 局部变量的生命周期是动态的每次从创建新变量的声明语句开始到不再引用为止,变量的存储空间可能被回收

    函数的参数变量和返回值变量都是局部变量,它们在函数每次被调用的时候创建。

    Go的GC判断变量是否回收的实现思路:从每个包级的变量、每个当前运行的函数的局部变量开始,通过指针和引用的访问路径遍历,是否可以找到该变量,如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响后续计算结果。

    示例:

    1
    var global *int
    2
    func f() {			
    3
    	var x int	
    4
    	x = 1
    5
    	global = &x
    6
    }
    7
    func g() {
    8
    	y := new(int)
    9
    	*y = 1
    10
    }

    上述的函数调用结果说明:

    • 虽然x变量定义在f函数内部,但是其必定在堆上分配,因为函数退出后仍然能通过包一级变量global找到,这样的变量,我们称之为从函数f中逃逸了
    • g函数返回时,变量*y不可达,因此没有从函数g中逃逸,其内存分配在栈上,会马上被被回收。(当然也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间)