go 语言类型

Go 语言中包括以下内置基础类型:

  • 布尔型:bool
  • 整型: int64 int32 int16 int8 uint8(byte) uint16 uint32 uint64 int uint
  • 浮点型:float32 float64
  • 复数型:complex64 complex128
  • 字符串:string
  • 字符型:rune
  • 错误型:error

Go 语言中包括以下内置复合类型:

  • 数组:array
  • 切片:slice (也是引用类型)
  • 指针:pointer
  • 字典:map(也是引用类型)
  • 通道:chan(也是引用类型)
  • 结构体:struct
  • 接口:interface(也是引用类型)

各个类型的零值:

  • 对于数字是 0
  • 对于布尔值是 false
  • 对于字符串是 ""
  • 对于接口和引用类型(slice,指针,map,通道,函数)是 nil
  • 对于一个数组或者结构体,零值是其所有成员的零值
整数

Go同时具备有符号数和无符号整数:

  • 有符号整数分4种大小,8位,16位,32位,64位:用int8int16int32int64,对应的无符号整数是uint8uint16uint32uint64
  • 此外还有2种类型intuint,这两种类型大小相等,都为32位或者64位,但不能认为它们一定就是32位或者64位,即使相同的硬件平台,不同的编译器也可能选用不同的大小。
  • 最后还有一种无符号整数uintptr,其大小和平台有关系,但足以完整存放指针。

注意在go中,整数与整数相除,如果除数为 0 会 panic

zero := 0
a := 1/zero  // panic: runtime error: integer divide by zero

浮点数

Go具有2种大小的浮点数 float32float64,十进制下,float32 的有效数字位数大约是6位, float64 有效数字大约是15位。
math包中给出了浮点值的极限:常量math.MaxFloat32是float32最大值,常量math.MaxFloat64是float64最大值。

math包种还有3个特殊的数:

  1. 正无穷大
  2. 负无穷大
  3. NaN (not a number)表示数学上无意义的运算结果
  • 浮点数的相等性比较
    var f float32 = 16777216
    fmt.Println(f == f+1)  // true
    

复数

TODO


布尔值

bool型的值只有2种可能:真(true) 或 假(false)。
和 C语言不同,布尔值无法隐式转换成数值(如0或1),反之也不行。

b := 1
if b {  // 编译错误
    // ... 
}

所以如果需要,最好是写成2个函数:

func btoi(b bool) int {
    if b {
        return 1
    }
    return 0
}

func itob(i int) bool {  return i != 0  }

常量

常量是一种表达式,其可以保证在编译阶段就计算出表达式的值,并不需要等到运行时。

const (
  e = 2.71828
  pi = 3.1415
)
  • 常量生成器
    常量的生成可以用使用常量生成器iota,它创建一系列相关值,而不是逐个显示写出,常量的声明中,iota从0开始取值,逐项加1。
    type Weekday int
    const (
        Sunday Weekday = itoa  // Sunday = 0
        Monday
        Tuesday
        //.....
    )
    
    const (
        _ = 1 << (10 * iota)
        KiB    // 1 << (10 * 1) = 1024
        MiB    // 1 << (10 * 2) = 1048576
        GiB
        //......
    )
    

字符串

字符串是不可变的 byte序列,其本身是一个复合结构,头部指向字节数组,没有NULL结尾,默认以UTF-8编码存储的Unicode码点序列。

type stringStruct {
    str unsafe.Pointer
    len int
}

内置的len函数返回字符串的字节数(并非文字符号的数目),cap不支持字符串类型参数。
下标访问操作s[i]则取得第 i 个字符,但要注意:字符串的第i个字节并不一定就是第i个字符,因为非ASCII字符的UTF-8码点需要两个字节或多个字节。

package main
import "fmt"
func main () {
    s := "长江"       
    fmt.Println(len(s))   // 6
    fmt.Println(s)         // 长江
    fmt.Println(s[0], s[1])  // 233 149
}

因为字符串不可变,所以字符串内部数据不允许修改。

s[0] = 'L'  // 编译错误

不可变,意味着两个字符串可以安全地共用一段底层内存,字符串s及其子串可以安全地共用数据。


image.png

这种特性使得复制任何长度字符串的开销都很低廉,类似,子串生成操作的开销也很低廉,因为这2种情况都没有分配新的内存。
子串生成操作s[i:j]产生一个新字符串,内容取自原字符串[i, j)的字节

  • 修改字符串
    如果要想修改字符串,则必须将其转换为可变类型([]rune[]byte),待完成转换之后,再转换回来,转换过程,都将发生一次内存拷贝

    b := []byte{'h','e', 'l', 'l', 'o'}
    s := string(b)    // 字节数组转字符串
    
    s := "hello"
    b := []byte(s)  // 字符串转字节数组
    
  • 字符串的拼接
    字符串的拼接时会触发内存分配和拷贝,单行语句拼接多个字符串时只分配一次内存

    s = s + "a" + "b"
    
  • UTF8

  • 字符串的遍历

    • 按字节遍历
      s := "中国"
      // byte
      for i := 0; i < len(s); i++ {
          fmt.Printf("%d: [%c]\t", i, s[i])
      }
      // 输出:
      0: [ä]  1: [¸]  2: [­]  3: [å]  4: [�]  5: [½]  
      
    • 按rune遍历
      rune,可以理解为是Unicode字符
      // 返回索引和Unicode字符
      for index, c := range s {
          fmt.Printf("%d: [%c]\t", index, c)
      }
      // 输出:
      0: [中]  3: [国]  
      
      这意味着:index 可能是不连续的。
  • 4个标准包对字符串操作比较重要:stringsbytesstrconvUnicode

    • strings
      用于搜索,替换,比较,修整,切分与连接字符串等
    • bytes
      用于操作字节slice([]byte类型)
    • strconv
      转换布尔值,整数,浮点数为与之对应的字符串的互相转换
      • 字符串转int
        s := "123"
        var i int
        var err error
        i, err = strconv.Atoi(s)
        
      • 字符串转int64
        s := "123"
        var i int64
        var err error
        i, err = strconv.ParseInt(s, 10, 64)
        
      • 字符转uint64
        s := "123"
        var i uint64
        var err error
        i, err = strconv.ParseUint(s, 10, 64)
        
      • 数字转字符串
        i := 123
        var s string
        s = strconv.Itoa(i)
        
    • Unicode
      判别文字符号值特性函数,如IsDigitIsLetterIsUpperIsLowerToUpperToLower

数组

数组是固定长度且拥有0个或者多个相同数据类型元素序列。

  • 初始化

    var s [3]int
    var q [3]int = [3]int{1, 2, 3}  // 数组字面量
    q := [...]int{1, 2, 3}    // 数组字面量
    r := [...]int{99: -1}    // 100个元素的数组,最后一个元素-1,其余是0 
    

    默认情况下,数组的长度是数组类型的一部分,所以[3]int[4]int是2种不同的数组类型,并且数组的长度必须 是常量表达式。
    内置函数 lencap返回数组的长度

  • 多维数组

    • 多维数组的类型包括每一维度的长度以及最终存储在元素中的数据类型
    • 在定义多维数组时,仅仅第一维度允许使用...
    • 内置函数 lencap 都返回第一维度的长度
  • 数组之间可以赋值
    在C/C++ 中数组之间不允许赋值,在Go中,同类型的数组是支持相互赋值的

    arrayA := [...]int {1, 2, 3}
    arrayB := arrayA   // arrayB持有一份拷贝,修改互不影响
    
  • 在函数间传递数组
    Go的数组在函数传参是值传递,意味着整个数组,都会被完整复制,所以为了效率,更好的是传递一个数组指针。

    func foo(ptr *[32]int) {
        ...
    }
    
  • 数组的比较
    在长度和类型相同的数组是可以使用==比较,长度不同或者类型不同的数组不能比较。


slice

slice 表示一个拥有相同类型元素地可变长度的序列,slice通常写作[]T,它的底层就是一个数组,对数组任意分隔,就可以得到一个切片。

array := [5]string{"a","b","c","d","e"}    // array 是数组
slice := array[:]    // 等同于原数组的切片
slice := array[2:5]                        // slice 是切片
slice := make([]int, 3)     // 使用make创建一个长度为3的切片
slice := make([]int, 3, 5)  // 使用make创建一个长度为3,容量为5的切片
  • slice有三个属性:指针,长度,容量

    array := [5]int{1, 2, 3, 4, 5}
    sliceA := array[1:2]  // 1, len(sliceA) = 1, cap(sliceA) = 4
    sliceB := array[2:4]  // 1, len(sliceB = 2, cap(sliceB) = 3
    
    • 指针
      指针指向数组的第一个可以从slice种访问的元素,它并不一定是底层数组的的第一个元素。
    • 长度
      长度是slice种元素个数,它不能超过slice的容量。可以使用len函数得到长度。
    • 容量
      从slice的起始元素到底层数组的最后一个元素间元素个数。可以使用cap函数得到容量。
      image.png

    因为slice包含了指向数组元素的指针,所以将一个slice传递给函数的时候,就可以在函数内部修改底层数组的元素。

  • nil 切片 和 空切片

    • nil 切片
      var slice []int
      s := []int(nil)  // 类型转换
      
      image.png
    • 空切片
      slice := make([]int, 0)
      slice := []int{}
      
    image.png
  • 切片切割

    s := []int{1, 2, 3, 4}
    s1 := s[1:3]    // s1 = s[2, 3] 即范围 [1:3) 左闭右开的范围。
    
  • slice无法比较
    和数组不同的是,slice无法比较,因此不能用==比较2个slice是否拥有相同的元素。
    对于[]byte可用标准库的bytes.Equal,但是对于其他类型,则必须自己函数实现。

    唯一允许的比较操作是和nil做比较,值为nil的slice没有对应的底层数组。

    var s []int  // len(s) == 0, s == nil
    s = nil    // len(s) == 0, s == nil
    s = []int(nil)  // 类型转换,len(s) == 0,
    s = []int{}    // len(s) == 0, s != nil
    
  • 切片迭代

    • 使用 range
      a := []int{1, 2, 4, 8}
      
      for idx, num := range a {
          fmt.Println(idx, num)
      }
      
      for idx := range a {
          fmt.Println(idx)
      }
      
    • 使用传统的 for 循环
      a := []int{1, 2, 4, 8}
      
      for i := 0; i < len(a); i++ {
          fmt.Println(a[i])
      }
      
  • slice 追加元素
    内置函数append用来将元素追加到slice后面,对 空切片 和 nil切片 都可以正常使用append函数

    • slice 追加元素
      slice := []string{"hello", "world"}
      str := "go"
      slice = append(slice, str)  // slice: [hello world go]
      
    • slice 追加另一个slice
      slice := []string{"hello", "world"}  
      sliceA := []string{"learn", "slice"}
      slice = append(slice, sliceA...)    // slice: [hello world go learn slice]
      

    使用 append 向 slice 追加元素时,如果slice空间不足,则会触发slice扩容,扩容实际上是重新分配一块更大的内存,将原slice的数据拷贝进新slice,然后再返回新slice,扩容后再将数据追加进去。

    func main() {
        var x, y []int
        for i := 0; i < 20; i++ {
            y = append(x, i)
            fmt.Printf("%d\tlen=%d\tcap=%d\tslice=%v\n", i, len(y), cap(y), y)
            x = y
        }
    }
    
    • 扩容策略

      • 如果原slice的容量小于1024,则新slice容量将扩大位原来的2倍
      • 如果原slice的容量大于或等于1024,则新slice容量将扩大位原来的1.25倍

      看得出来,上面的扩容策略是为了在避免频繁的扩容和避免浪费空间之间的平衡。

  • slice 拷贝
    可以使用内置的copy将源切片的数据逐个拷贝到目的切片指向的数组中,注意:拷贝过程中不会发生扩容,拷贝的数量取决于两个切片长度的最小值。

    b2 := make([]byte, len(b))
    copy(b2, b)
    
  • slice 插入元素

    • 开头插入
      var a = []int{1, 2, 3}
      a = append([]int{0}, a...)  // 在开头添加一个元素
      
      需要注意的是,在切片开头插入元素,一般都会导致内存的重新分配和已有元素的全部复制一次。
    • 中间插入
      var a = []int{1, 2, 3}
      
      a = append(a, 0)    // 扩充空间
      copy(a[i+1:], a[i])  // a[i:] 向后移动一个元素
      a[i] = 10  // 设置新的元素
      
  • slice 删除元素

    • 开头或者结尾删除
      var a = []int{1, 2, 3}
      a = a[1:]  // 开头删除
      a = a[:len(a)-1]  // 结尾删除  
      
    • 中间删除
      var a = []int{1, 2, 3, 4, 5, 6}
      a = a[:2 + copy(a[2:], a[2+1:])]    // 删除第2个元素, 公式:  a[: i + copy(a[i:], a[i+1:])]
      fmt.Println("a: ", a)  // a:  [1 2 4 5 6]
      
      var b = []int{1, 2, 3, 4, 5, 6}
      b = b[:1 + copy(b[1:], b[1+3:])]    // 删除[1, 1+3)元素, 公式:  b[: i + copy(b[i:], b[i+N:])]
      fmt.Println("b: ", b)  // a:  [1 2 4 5 6]
      
  • slice 的排序和查找

还有slice的其他操作可参考:go slice 操作常用技巧


map

在Go语言种,map是散列表的引用,map的类型是map[K]V,其中K和V是字典的键和值对应的数据类型,键的类型K,必须是可以通过操作符==来进行比较的数据类型。

  • 创建

    // 使用字面量的方式创建
    ages := map[string]int{
        "alice": 34,
        "charlie": 34,
    }
    
    ages := make(map[string]int)
    
  • 内置函数len 返回当前键值对数量,cap不支持 map 类型

  • 遍历

    for name, age : range ages {
        fmt.Printf("%s\t%d\n", name, age)
    }
    

    map中的元素的迭代顺序是不固定的,每次遍历都可能得到不同的元素的顺序。

    func main() {
      m := map[int]int{1: 1, 2: 2, 3: 3, 4: 4, 5:5, 6:6, 7:7}
      print(m)
    
      time.Sleep(time.Second * 5)
      fmt.Println("----------------")
    
      print(m)
    }
    
    func print(m map[int]int) {
      for k, v := range m {
          fmt.Println(k, v)
      }
    }
    
    image.png

如果需要按照某种顺序来遍历map中的元素,那么可以将 key 放入 slice 中排序得到。

  • 查找
    通过下标操作访问map中的元素,会有2种返回值类型:

    age := ages["bob"]
    

    这种将会返回value,如果key不存在,则value会是零值。

    再或者额外返回一个bool值,用来表示该key是否存在

    age, exist := ages["bob"]; 
    if exist {
        //....
    }
    

    返回的value 是一份拷贝,对其修改并不会改变 map 中对应的 value。

  • 添加

    age := ages["bob"]
    

    注意:map的零值是nil,表示没有引用任何散列表,向零值map设置元素会导致panic。

  • 修改
    修改和添加类似,也是用下标访问key直接修改value,但需要注意的是,当key不存在时,会直接创建key,value 则为零值

    age := ages["bob"]  // bob 不存在,将会直接创建
    
  • 删除
    使用内置函数delete,来从字典中根据键值移除元素,健不存在时,不会报错。

    delete(ages, "alice")
    
  • 快速清空map元素

    m = make(map[T1]T2)  // 直接make一个新的map,原来的map由gc回收。
    
  • map 无法比较
    和slice一样,map无法比较,唯一合法地就是和nil比较。

    var a map[string]int          // a is nil len(a) == 0
    a = nil                       // a is nil len(a) == 0
    c := make(map[string]int)     // c is not nil len(c) == 0
    d := map[string]int{}         // c is not nil len(c) == 0
    

    如果我们想要比较2个map,则可以采用如下实现:

    func equal(x, y map[string]int) bool {
        if len(x) != len(y) {
            return false
        }
        for k, xv := range x {
            if yv, ok := y[k]; !ok || yv != xv {    // 这里不能直接写为:xv != y[k]
                return false
            }
        }
        return true
    }
    
    equal(map[string]{"A": 0}, map[string]int{"B": 42})
    
  • map 的并发安全性
    map 不是并发安全的,所以多协程下读写需要加锁。 这里提一个比较少见的但容易犯错的场景:

    var m = map[string]int{}
    var lock sync.Mutex
    
    func write() {
        for i := 0; i != 1000000; i++ {
            time.Sleep(time.Microsecond * 100)
            lock.Lock()
            m[tool.GenerateRandomString(5)] = tool.GenerateRandom(10, 100)
            lock.Unlock()
        }
    }
    
    func read() {
        for i := 0; i != 1000000; i++ {
            time.Sleep(time.Microsecond * 200)
            fmt.Println(fmt.Sprintf("%+v", m))    // +v 会遍历map,这里会造成map的并发读写,进而panic
        }
    }
    
    func main() {
        go write()
        go read()
        
        select {}
    }
    
Set

go 中自带的标准库只有map,而没有set,那么我们可以利用 mapstruct{} size 为0 来简单封装实现一个 set

package main

import "fmt"

type Set map[string]struct{}

func NewSet() Set {
    s := make(map[string]struct{})
    return Set(s)
}

func NewSetWithInitSize(size int) Set {
    s := make(map[string]struct{}, size)
    return Set(s)
}

func (s Set) Has(key string) bool {
    _, exist := s[key]
    return exist
}

func (s Set) Add(key string) {
    s[key] = struct{}{}
}

func (s Set) Delete(key string) {
    delete(s, key)
}

func main() {
    s := NewSet()

    s.Add("hello")
    s.Add("world")
    s.Add("go")

    exist := s.Has("hello")
    if exist {
        fmt.Println("exist")
    }

    exist = s.Has("world")
    if exist {
        fmt.Println("exist")
    }

    s.Delete("go")

    exist = s.Has("go")
    if exist {
        fmt.Println("exist")
    } else {
        fmt.Println("not exist")
    }
}

更加完备的set,可以参考开源:golang-set


指针

获取一个变量的指针非常容易,使用取地址符 & 就可以

name := "hello world"
nameP := &name 
  • 指针的操作
    在 Go 语言中指针的操作无非是两种:一种是获取指针指向的值,一种是修改指针指向的值。

  • 什么时候使用指针?

    • 不要对 map、slice、channel 这类引用类型使用指针;
    • 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
    • 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
    • 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;

Go语言中的 new 和 make 的区别

TODO

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容

  • 定义: 关键词var用于定义变量,和C不同,类型被放在变量类型的后面。 除了var 关键词外,也可以同通过简短模式...
    LegendGo阅读 198评论 1 0
  • 官方参考 https://golang.org/ref/spec#Types 其中类型有: Method sets...
    linanwx阅读 999评论 0 0
  • 序 一直在关注Golang的发展,最近Go 1.7也发布了,于是下定决心学习Golang。本文是《Go 学习笔记》...
    技术学习阅读 961评论 0 1
  • 3. Go 数据类型及数据结构 前面的几节中有意无意地创建了很多变量,在变量声明过程中,除非声明过程就初始化,否则...
    PRE_ZHY阅读 656评论 0 2
  • 类型转换 Go语言和C语言不一样,Go语言没有隐式转换,只有显示转换 格式和C语言一样 Go语言类型转换被用于转换...
    AuglyXu阅读 1,288评论 0 0