[TOC]

主要特征

  • 语法简单,自带 gc
  • 静态编译,编译好后在服务器直接运行
  • 简单的思想,没有继承、多态、类等
  • 语法层支持并发,拥有同步并发的 channel 类型,使并发开发变得非常方便
  • 内置类型丰富,函数多返回值
  • 反射

Golang 内置类型和函数

内置类型

值类型

bool
int(32 or 64), int8, int16, int32, int64
uint(32 or 64), uint8(byte), uint16, uint32, uint64
float32, float64
string
complex64, complex128
array    -- 固定长度的数组

引用类型(指针类型)

slice   -- 序列数组(最常用)
map     -- 映射
chan    -- 管道

内置函数

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。

append          -- 用来追加元素到数组slice中,返回修改后的数组slice
close           -- 主要用来关闭channel
delete            -- 从map中删除key对应的value
panic            -- 停止常规的goroutine  panic和recover用来做错误处理
recover         -- 允许程序定义goroutine的panic动作
imag            -- 返回complex的实部   complexreal imag用于创建和操作复数
real            -- 返回complex的虚部
make            -- 用来分配内存返回Type本身(只能应用于slice, map, channel)
new                -- 用来分配内存主要用来分配值类型比如intstruct返回指向Type的指针
cap                -- capacity是容量的意思用于返回某个类型的最大容量只能用于切片和 map
copy            -- 用于复制和连接slice返回复制的数目
len                -- 来求长度比如stringarrayslicemapchannel 返回长度
printprintln     -- 底层打印函数在部署环境中建议使用 fmt 

内置接口

type error interface { //只要实现了Error()函数,返回值为String的都实现了err接口

        Error()    String

}

init 函数和 main 函数

init 函数

go 语言中 init 函数用于包(package)的初始化,该函数是 go 语言的一个重要特性。

init 函数有如下特征:

  1. init 函数是用于程序执行前做包初始化的函数,比如初始化包里的变量
  2. 每个包可以拥有多个 init 函数
  3. 包的每个源文件也可以拥有多个 init 函数
  4. 同一个包中多个 init 函数的执行顺序 go 语言没有明确的说明
  5. 不同包的 init 函数按照包导入的依赖关系决定该初始化函数的执行顺序
  6. init 函数不能被其他函数调用,而是在 main 函数执行之前自动被调用

main 函数

Go语言程序的默认入口函数(主函数):func main()

func main() {
	// 函数体
}

两种函数的异同

  • 相同点:

    • 两个函数在定义时不能有任何的参数和返回值,且 Go 程序自动调用
  • 不同点:

    • init 函数可以应用于任意包中,且可以重复定义多个
    • main 函数只能用于 main 包中,且只能定义一个
  • 两个函数的执行顺序:

    • 同一个 go 文件中的 init() 调用顺序是从上到下
    • 同一个 package 中不同文件是按文件名字符串比较 “从小到大” 顺序调用各文件中的 init() 函数
    • 对于不同的 package ,如果不相互依赖的话,按照 main 包中 “先 import 的后调用” 的顺序调用包中的 init() ,如果 package 存在依赖,则先调用最早被以来的 package 中的 init() ,最后调用 main 函数

运算符

  1. 算术运算符
运算符描述
+相加
-相减
*相乘
/相除
%求余

注意: ++(自增)和–(自减)在Go语言中是单独的语句,并不是运算符

  1. 关系运算符
运算符描述
==检查两个值是否相等,如果相等返回 True 否则返回 False
!=检查两个值是否不相等,如果不相等返回 True 否则返回 False
>检查左边值是否大于右边值,如果是返回 True 否则返回 False
>=检查左边值是否大于等于右边值,如果是返回 True 否则返回 False
<检查左边值是否小于右边值,如果是返回 True 否则返回 False
<=检查左边值是否小于等于右边值,如果是返回 True 否则返回 False
  1. 逻辑运算符
运算符描述
&&逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False
||逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False
!逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True
  1. 位运算符
运算符描述
&参与运算的两数各对应的二进位相与(两位均为1才为1)
|参与运算的两数各对应的二进位相或(两位有一个为1就为1)
^参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1(两位不一样则为1)
«左移 n 位就是乘以 2 的 n 次方。“a « b” 是把 a 的各二进位全部左移 b 位,高位丢弃,低位补 0
»右移 n 位就是除以 2 的 n 次方。“a » b” 是把 a 的各二进位全部右移 b 位
  1. 赋值运算符
运算符描述
=将一个表达式的值赋给一个左值
+=相加后再赋值
-=相减后再赋值
*=相乘后再赋值
/=相除后再赋值
%=求余后再赋值
«=左移后赋值
»=右移后赋值
&=按位与后赋值
|=按位或后赋值
^=按位异或后赋值

下划线

“_” 是特殊标识符,用来忽略结果

  1. 下划线在 import

    • 当导入一个包时,该包下的文件里所有的 init() 函数都会被执行。如果我们仅仅希望它执行 init() 函数,不想把整个包都导入进来,就可以使用 import _ 包路径 来引用该包

    • 示例 1:

      • 代码结构

        src 
        |
        +--- main.go            
        |
        +--- hello
               |
                +--- hello.go
        
        package main
        
        import _ "./hello"
        
        func main() {
            // hello.Print() 
            //编译报错:./main.go:6:5: undefined: hello
        }
        
      • hello.go

        package hello
        
        import "fmt"
        
        func init() {
            fmt.Println("imp-init() come here.")
        }
        
        func Print() {
            fmt.Println("Hello!")
        }
        
      • 输出结果:

        imp-init() come here.
        
    • 示例 2:

      import "database/sql"
      import _ "github.com/go-sql-driver/mysql"
      

      第二个 import 就是不直接使用 mysql 包,只是执行这个包的 init() 函数,把 mysql 的驱动注册到 sql 包里,然后程序里就可以使用 sql 包来访问 mysql 数据库了

  2. 下划线在代码中

    package main
    
    import (
        "os"
    )
    
    func main() {
        buf := make([]byte, 1024)
        f, _ := os.Open("/Users/***/Desktop/text.txt")
        defer f.Close()
        for {
            n, _ := f.Read(buf)
            if n == 0 {
                break    
    
            }
            os.Stdout.Write(buf[:n])
        }
    }
    
    • 解释 1:下划线意思是忽略这个变量,比如: os.Open ,返回值为 *os.File, error
      • 普通写法是:f, err := os.Open("xxxxxx")
      • 如果此时不需要知道返回的错误值,就可以用:f, _ := os.Open("xxxxxx") ,如此就忽略了 error 变量
    • 解释 2:下划线作为占位符,意思是那个位置本应赋给某个值,但是咱们不需要这个值,所以就把该值赋给下划线,意思是丢掉不要,这样编译器可以更好的优化。
      • 任何类型的单个值都可以丢给下划线,这种情况是占位用的,比如方法返回两个结果,而你只想要一个结果,那另一个就用 “_” 占位

变量与常量

变量

Go 语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。并且 Go 语言的变量声明后必须使用

Go 语言的变量声明格式为:

var 变量名 变量类型

变量声明以关键字 var 开头,变量类型放在变量的后面,行尾无需分号:

var name string
var age int
var ok bool

每声明一个变量就写一个 var 比较繁琐,Go 语言还支持批量变量声明:

var (
	a string
	b int
	c bool
	d float32
)

Go 语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为 0 ,字符串变量的默认值为空字符串,布尔型变量默认为false。切片、函数、指针变量的默认为 nil

当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:

var 变量名 类型 = 表达式

// example
var name string = "hello"
var sex int = 1

或者一次初始化多个变量:

var name, sex = "hello", 1

类型推导:有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化

var name = "hello"
var sex = 1

短变量声明:在函数内部,可以使用更简略的 := 方式声明并初始化变量

package main

import (
    "fmt"
)
// 全局变量m
var m = 100

func main() {
    n := 10
    m := 200 // 此处声明局部变量m
    fmt.Println(m, n)
}

匿名变量:在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线 _ 表示

func foo() (int, string) {
    return 10, "hello"
}
func main() {
    x, _ := foo()
    _, y := foo()
    fmt.Println("x=", x)
    fmt.Println("y=", y)
}

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明(在 Lua 等编程语言里,匿名变量也被叫做哑元变量)

注意事项:

  • 函数外的每个语句必须以关键字开始(var、const、func 等)
  • := 不能在函数外使用
  • _ 多用于占位,表示忽略值

常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把 var 换成了 const常量在定义的时候必须赋值

const pi = 3.1415
const e = 2.7182

声明了 pie 这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了

多个常量也可以一起声明:

const (
	pi = 3.1415
	e = 2.7182
)

const 同时声明多个常量时,如果省略了值则表示和上面一行的值相同

const (
	n1 = 100
	n2
	n3
)

常量 n1n2n3 的值均为 100

iotaiota 是 Go 语言的常量计数器,只能在常量的表达式中使用。 iotaconst 关键字出现时将被重置为 0 。const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。 使用 iota 能简化定义,在定义枚举时很有用

const (
	n1 = 100 // 0
	n2		 // 1
	n3		 // 2
)

使用 _ 跳过某些值:

const (
	n1 = 100 // 0
	n2		 // 1
	_		 
	n3		 // 3
)

使用示例:

const (
	n1 = iota // 0
	n2 = 100
	n3 = iota // 2
	n4 // 3
)
const n5 = iota // 0

定义数量级:

const (
    _  = iota
    KB = 1 << (10 * iota)
    MB = 1 << (10 * iota)
    GB = 1 << (10 * iota)
    TB = 1 << (10 * iota)
    PB = 1 << (10 * iota)
)

多个 iota 定义在一行:

const (
    a, b = iota + 1, iota + 2 //1,2
    c, d                      //2,3
    e, f                      //3,4
)

基本类型

  1. 整形:整型分为以下两个大类: 按长度分为:int8int16int32int64对应的无符号整型:uint8uint16uint32uint64 。其中,uint8 就是 byte 型,int16 对应C语言中的 short 型,int64 对应C语言中的 long

  2. 浮点型:Go语言支持两种浮点型数:float32float64 。这两种浮点型数据格式遵循 IEEE 754 标准: float32 的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64

  3. 复数complex64complex128 ,复数有实部和虚部,complex64 的实部和虚部为 32 位,complex128 的实部和虚部为 64 位

  4. 布尔值:Go语言中以 bool 类型进行声明布尔型数据,布尔型数据只有 truefalse 两个值

    • 注意:布尔类型变量的默认值为 falseGo 语言中不允许将整型强制转换为布尔型,布尔型无法参与数值运算,也无法与其他类型进行转换
  5. 字符串:Go 语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。 Go 语言里的字符串的内部实现使用 UTF-8 编码。 字符串的值为双引号中的内容,可以在Go语言的源码中直接添加非 ASCII 码字符,例如:

    s1 := "hello"
    s2 := "你好" // 非 ASCII码
    
  6. 多行字符串:Go 语言中使用反引号定义多行字符串,例如:

    s1 := `第一行
    第二行
    第三行
    `
    fmt.Println(s1)
    
  7. 字符串常用操作

方法介绍
len(str)求长度
+ 或 fmt.Sprintf拼接字符串
strings.Split分割字符串
strings.Contains判断是否包含
strings.HasPrefix, strings.HasSuffix前缀/后缀判断
strings.Index(), strings.LastIndex()子串出现的位置
strings.Join(a[]string, sep string)join 操作
  1. byte 和 rune 类型

    • 组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号 包裹起来,如:

      var a := '中'
      var b := 'x'
      
    • Go 语言的字符有以下两种:

      • uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符
      • rune 类型,代表一个 UTF-8 字符
    • 当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型实际是一个 int32 。 Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾

      // 遍历字符串
      func traversalString() {
          s := "pprof.cn博客"
          for i := 0; i < len(s); i++ { //byte
              fmt.Printf("%v(%c) ", s[i], s[i])
          }
          fmt.Println()
          for _, r := range s { //rune
              fmt.Printf("%v(%c) ", r, r)
          }
          fmt.Println()
      }
      

      输出如下:

      112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 229(å) 141() 154() 229(å) 174(®) 162(¢)
      112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 21338(博) 23458(客)
      
      • 因为 UTF8 编码下一个中文汉字由 3~4 个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果
      • 字符串底层是一个 byte 数组,所以可以和 []byte 类型相互转换。 rune 类型用来表示 UTF-8 字符,一个 rune 字符由一个或多个 byte 组成,注意:字符串是不能修改的
  2. 修改字符串:要修改字符串,需要先将其转换成 []rune[]byte ,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组

    func changeString() {
        s1 := "hello"
        // 强制类型转换
        byteS1 := []byte(s1)
        byteS1[0] = 'H'
        fmt.Println(string(byteS1)) // 输出:Hello
    
        s2 := "博客"
        runeS2 := []rune(s2)
        runeS2[0] = '狗'
        fmt.Println(string(runeS2)) // 输出:狗客
    }
    
  3. 类型转换:Go 语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用

    • 基本语法:

      T(表达式)
      

      其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等

      比如计算直角三角形的斜边长时使用 math 包的 Sqrt() 函数,该函数接收的是 float64 类型的参数,而变量 a 和 b 都是 int 类型的,这个时候就需要将 a 和 b 强制类型转换为 float64 类型

      func sqrtDemo() {
          var a, b = 3, 4
          var c int
          // math.Sqrt()接收的参数是float64类型,需要强制转换
          c = int(math.Sqrt(float64(a*a + b*b)))
          fmt.Println(c)
      }
      

数组 Array

  1. 数组定义:

    var a [len]int
    
    • 比如:var a [5]int ,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变
    • 长度是数组类型的一部分,因此,var a[5]intvar a[10]int 是不同的类型
    • 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值
    • 指针数组 [n]*T,数组指针 *[n]T
  2. 一维数组

    // 全局:
    var arr0 [5]int = [5]int{1, 2, 3}
    var arr1 = [5]int{1, 2, 3, 4, 5}
    var arr2 = [...]int{1, 2, 3, 4, 5, 6}
    var str = [5]string{3: "hello world", 4: "tom"}
    // 局部:
    a := [3]int{1, 2}           // 未初始化元素值为 0。
    b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
    c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
    d := [...]struct {
        name string
        age  uint8
    }{
        {"user1", 10}, // 可省略元素类型。
        {"user2", 20}, // 别忘了最后一行的逗号。
    }
    
    package main
    
    import (
        "fmt"
    )
    
    var arr0 [5]int = [5]int{1, 2, 3}
    var arr1 = [5]int{1, 2, 3, 4, 5}
    var arr2 = [...]int{1, 2, 3, 4, 5, 6}
    var str = [5]string{3: "hello world", 4: "tom"}
    
    func main() {
        a := [3]int{1, 2}           // 未初始化元素值为 0。
        b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
        c := [5]int{2: 100, 4: 200} // 使用引号初始化元素。
        d := [...]struct {
            name string
            age  uint8
        }{
            {"user1", 10}, // 可省略元素类型。
            {"user2", 20}, // 别忘了最后一行的逗号。
        }
        fmt.Println(arr0, arr1, arr2, str)
        fmt.Println(a, b, c, d)
    }
    
    输出结果:
    [1 2 3 0 0] [1 2 3 4 5] [1 2 3 4 5 6] [   hello world tom]
    [1 2 0] [1 2 3 4] [0 0 100 0 200] [{user1 10} {user2 20}]
    
  3. 多维数组 & 多维数组遍历

    • 参考:https://www.topgoer.com/go%E5%9F%BA%E7%A1%80/%E6%95%B0%E7%BB%84Array.html
  4. 数组拷贝和传参

    package main
    
    import "fmt"
    
    func printArr(arr *[5]int) {
        arr[0] = 10
        // 循环中 i 是元素下标,v 是元素数值
        for i, v := range arr {
            fmt.Println(i, v)
        }
    }
    
    func main() {
        var arr1 [5]int
        printArr(&arr1)
        fmt.Println(arr1)
        arr2 := [...]int{2, 4, 6, 8, 10}
        printArr(&arr2)
        fmt.Println(arr2)
    }
    
    输出结果:
    0 10
    1 0
    2 0
    3 0
    4 0
    [10 0 0 0 0]
    0 10
    1 4
    2 6
    3 8
    4 10
    [10 4 6 8 10]
    

切片 Slice

  1. slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案

    • 切片是数组的一个引用,但自身是结构体,通过值拷贝传递

    • 切片的长度可以改变,因此切片是一个可变数组

    • 切片遍历方式和数组一样,可以用 len() 求长度,表示可用元素数量,读写操作不能超过该限制

    • cap() 可以求出切片最大扩张容量,不能超出数组限制:0 <= len(slice) <= len(array) ,其中 arrayslice 引用的数组

    • 定义:var 变量名 []类型 ,比如:var str []stringvar arr []int

    • 如果 slice == nil ,那么 len()、cap() 结果都等于 0

  2. 创建切片的各种方式

    package main
    
    import "fmt"
    
    func main() {
       //1.使用 var 声明切片 s1,s1 是一个 nil 切片,没有分配内存
       var s1 []int
       if s1 == nil {
          fmt.Println("是空")
       } else {
          fmt.Println("不是空")
       }
       // 2.使用 := 简短声明,创建切片 s2 并初始化为空切片
       // 空切片与 nil 切片不同,空切片底层数组已分配内存,但是容量为 0,而 nil 切片没有分配内存
       s2 := []int{}
       // 3.使用 make() 创建切片,可以指定切片长度和容量
       var s3 []int = make([]int, 0)
       fmt.Println(s1, s2, s3)
       // 4.初始化赋值
       var s4 []int = make([]int, 0, 0)
       fmt.Println(s4)
       s5 := []int{1, 2, 3}
       fmt.Println(s5)
       // 5.从数组切片
       arr := [5]int{1, 2, 3, 4, 5}
       var s6 []int
       // 前包后不包
       s6 = arr[1:4]
       fmt.Println(s6)
    }
    

    注意:切片的长度是当前切片存储的元素数量,通过 len() 获取切片长度;切片容量是从切片的起始位置到底层数组的末尾的元素数量,也就是切片能容纳的元素的最大数量,通过 cap() 获取切片长度

  3. 切片初始化

    // 全局:
    var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    var slice0 []int = arr[start:end] 
    var slice1 []int = arr[:end]        
    var slice2 []int = arr[start:]        
    var slice3 []int = arr[:] 
    var slice4 = arr[:len(arr)-1]      //去掉切片的最后一个元素
    // 局部:
    arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
    slice5 := arr[start:end]
    slice6 := arr[:end]        
    slice7 := arr[start:]     
    slice8 := arr[:]  
    slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
    

    image.png

    package main
    
    import (
        "fmt"
    )
    
    var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    var slice0 []int = arr[2:8]
    var slice1 []int = arr[0:6]        //可以简写为 var slice []int = arr[:end]
    var slice2 []int = arr[5:10]       //可以简写为 var slice[]int = arr[start:]
    var slice3 []int = arr[0:len(arr)] //var slice []int = arr[:]
    var slice4 = arr[:len(arr)-1]      //去掉切片的最后一个元素
    func main() {
        fmt.Printf("全局变量:arr %v\n", arr)
        fmt.Printf("全局变量:slice0 %v\n", slice0)
        fmt.Printf("全局变量:slice1 %v\n", slice1)
        fmt.Printf("全局变量:slice2 %v\n", slice2)
        fmt.Printf("全局变量:slice3 %v\n", slice3)
        fmt.Printf("全局变量:slice4 %v\n", slice4)
        fmt.Printf("-----------------------------------\n")
        arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
        slice5 := arr[2:8]
        slice6 := arr[0:6]         //可以简写为 slice := arr[:end]
        slice7 := arr[5:10]        //可以简写为 slice := arr[start:]
        slice8 := arr[0:len(arr)]  //slice := arr[:]
        slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
        fmt.Printf("局部变量: arr2 %v\n", arr2)
        fmt.Printf("局部变量: slice5 %v\n", slice5)
        fmt.Printf("局部变量: slice6 %v\n", slice6)
        fmt.Printf("局部变量: slice7 %v\n", slice7)
        fmt.Printf("局部变量: slice8 %v\n", slice8)
        fmt.Printf("局部变量: slice9 %v\n", slice9)
    }
    
    输出结果:
    全局变量:arr [0 1 2 3 4 5 6 7 8 9]
    全局变量:slice0 [2 3 4 5 6 7]
    全局变量:slice1 [0 1 2 3 4 5]
    全局变量:slice2 [5 6 7 8 9]
    全局变量:slice3 [0 1 2 3 4 5 6 7 8 9]
    全局变量:slice4 [0 1 2 3 4 5 6 7 8]
    -----------------------------------
    局部变量: arr2 [9 8 7 6 5 4 3 2 1 0]
    局部变量: slice5 [2 3 4 5 6 7]
    局部变量: slice6 [0 1 2 3 4 5]
    局部变量: slice7 [5 6 7 8 9]
    局部变量: slice8 [0 1 2 3 4 5 6 7 8 9]
    局部变量: slice9 [0 1 2 3 4 5 6 7 8]
    
  4. 通过 make 来创建切片

    var slice []type = make([]type, len)
    slice := make([]type, len)
    slice := make([]type, len, cap)
    

    image.png

    切片的内存布局如下:

    image.png

    读写操作实际目标是切片指向的底层数组,需要注意索引号的差别

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        data := [...]int{0, 1, 2, 3, 4, 5}
    
        s := data[2:4]
        s[0] += 100
        s[1] += 200
    
        fmt.Println(s)
        fmt.Println(data)
    }
    
    输出如下:
    [102 203]
    [0 1 102 203 4 5]
    

    可直接创建 slice 对象,自动分配底层数组

    package main
    
    import "fmt"
    
    func main() {
        s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。
        fmt.Println(s1, len(s1), cap(s1))
    
        s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
        fmt.Println(s2, len(s2), cap(s2))
    
        s3 := make([]int, 6) // 省略 cap,相当于 cap = len。
        fmt.Println(s3, len(s3), cap(s3))
    }
    

    输出如下:

    [0 1 2 3 0 0 0 0 100] 9 9
    [0 0 0 0 0 0] 6 8
    [0 0 0 0 0 0] 6 6
    

    使用 make 动态创建 slice,避免了数组必须用常量做长度的麻烦,还可用指针直接访问底层数组,退化成普通数组操作

    package main
    
    import "fmt"
    
    func main() {
        s := []int{0, 1, 2, 3}
        p := &s[2] // *int, 获取底层数组元素指针。
        *p += 100
    
        fmt.Println(s)
    }
    

    输出:

    [0 1 102 3]
    
  5. 使用 append 操作切片

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        var a = []int{1, 2, 3}
        fmt.Printf("slice a : %v\n", a)
        var b = []int{4, 5, 6}
        fmt.Printf("slice b : %v\n", b)
        c := append(a, b...)
        fmt.Printf("slice c : %v\n", c)
        d := append(c, 7)
        fmt.Printf("slice d : %v\n", d)
        e := append(d, 8, 9, 10)
        fmt.Printf("slice e : %v\n", e)
    
    }
    

    输出:

    slice a : [1 2 3]
    slice b : [4 5 6]
    slice c : [1 2 3 4 5 6]
    slice d : [1 2 3 4 5 6 7]
    slice e : [1 2 3 4 5 6 7 8 9 10]
    

    append: 向 slice 尾部添加数据,返回新的 slice 对象

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        s1 := make([]int, 0, 5)
        fmt.Printf("%p\n", &s1)
    
        s2 := append(s1, 1)
        fmt.Printf("%p\n", &s2)
    
        fmt.Println(s1, s2)
    
    }
    

    输出:

    0xc42000a060
    0xc42000a080
    [] [1]
    
  6. 超出原 slice.cap 限制,就会重新分配底层数组,让该 slice 引用新的底层数组,即使原数组并未填满

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        data := [...]int{0, 1, 2, 3, 4, 10: 0}
        s := data[:2:3]
    
        s = append(s, 100, 200) // 一次 append 两个值,超出 s.cap 限制。
    
        fmt.Println(s, data)         // 重新分配底层数组,与原数组无关。
        fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。
    
    }
    

    输出:

    [0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
    0xc4200160f0 0xc420070060
    

    go 通常以 2 倍容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。或初始化足够长的 len 属性,改用索引号进行操作。及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收

  7. slice 中 cap 重新分配规律

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        s := make([]int, 0, 1)
        c := cap(s)
    
        for i := 0; i < 50; i++ {
            s = append(s, i)
            if n := cap(s); n > c {
                fmt.Printf("cap: %d -> %d\n", c, n)
                c = n
            }
        }
    
    }
    

    输出:

    cap: 1 -> 2
    cap: 2 -> 4
    cap: 4 -> 8
    cap: 8 -> 16
    cap: 16 -> 32
    cap: 32 -> 64
    
  8. 切片拷贝

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        s1 := []int{1, 2, 3, 4, 5}
        fmt.Printf("slice s1 : %v\n", s1)
        s2 := make([]int, 10)
        fmt.Printf("slice s2 : %v\n", s2)
        copy(s2, s1)
        fmt.Printf("copied slice s1 : %v\n", s1)
        fmt.Printf("copied slice s2 : %v\n", s2)
        s3 := []int{1, 2, 3}
        fmt.Printf("slice s3 : %v\n", s3)
        s3 = append(s3, s2...)
        fmt.Printf("appended slice s3 : %v\n", s3)
        s3 = append(s3, 4, 5, 6)
        fmt.Printf("last slice s3 : %v\n", s3)
    
    }
    

    输出:

    slice s1 : [1 2 3 4 5]
    slice s2 : [0 0 0 0 0 0 0 0 0 0]
    copied slice s1 : [1 2 3 4 5]
    copied slice s2 : [1 2 3 4 5 0 0 0 0 0]
    slice s3 : [1 2 3]
    appended slice s3 : [1 2 3 1 2 3 4 5 0 0 0 0 0]
    last slice s3 : [1 2 3 1 2 3 4 5 0 0 0 0 0 4 5 6]
    

    copy: 函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠

    应及时讲所需数据 copy 到较小的 slice,以便释放超大号底层数组内存

  9. slice 遍历

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
        slice := data[:]
        for index, value := range slice {
            fmt.Printf("inde : %v , value : %v\n", index, value)
        }
    
    }
    

    输出:

    inde : 0 , value : 0
    inde : 1 , value : 1
    inde : 2 , value : 2
    inde : 3 , value : 3
    inde : 4 , value : 4
    inde : 5 , value : 5
    inde : 6 , value : 6
    inde : 7 , value : 7
    inde : 8 , value : 8
    inde : 9 , value : 9
    
  10. 切片 resize

package main

import (
    "fmt"
)

func main() {
    var a = []int{1, 3, 4, 5}
    fmt.Printf("slice a : %v , len(a) : %v\n", a, len(a))
    b := a[1:2]
    fmt.Printf("slice b : %v , len(b) : %v\n", b, len(b))
    c := b[0:3]
    fmt.Printf("slice c : %v , len(c) : %v\n", c, len(c))
}

输出:

slice a : [1 3 4 5] , len(a) : 4
slice b : [3] , len(b) : 1
slice c : [3 4 5] , len(c) : 3
  1. 字符串与切片

    string 底层是一个 byte 数组,因此也可以进行切片操作

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        str := "hello world"
        s1 := str[0:5]
        fmt.Println(s1)
    
        s2 := str[6:]
        fmt.Println(s2)
    }
    

    输出:

    hello
    world
    

    string 本身是不可变的,要改变 string 中的字符,需要如下操作:

    • 英文字符串:

      package main
      
      import (
          "fmt"
      )
      
      func main() {
          str := "Hello world"
          s := []byte(str) //中文字符需要用[]rune(str)
          s[6] = 'G'
          s = s[:8]
          s = append(s, '!')
          str = string(s)
          fmt.Println(str)
      }
      

      输出:

      Hello Go!
      
    • 含有中文字符

      package main
      
      import (
          "fmt"
      )
      
      func main() {
          str := "你好,世界!hello world!"
          s := []rune(str) 
          s[3] = '够'
          s[4] = '浪'
          s[12] = 'g'
          s = s[:14]
          str = string(s)
          fmt.Println(str)
      }
      

      输出:

      你好,够浪!hello go
      

Map

map 是一种无序的基于 k-v 的数据结构,Go 语言中的 map 是 引用类型,必须初始化才能使用

  1. map 定义

    map[KeyType]ValueType
    

    其中 KeyType 表示键类型,ValueType 表示值类型

    map 类型的变量默认初始值为 nil,需要使用 make() 函数来分配内存:

    make(map[KeyType]ValueType, [cap])
    

    其中 cap 表示 map 的容量,该参数非必须

  2. map 基本使用

    func main() {
        scoreMap := make(map[string]int, 8)
        scoreMap["张三"] = 90
        scoreMap["小明"] = 100
        fmt.Println(scoreMap)
        fmt.Println(scoreMap["小明"])
        fmt.Printf("type of a:%T\n", scoreMap)
    } 
    

    输出:

    map[小明:100 张三:90]
    100
    type of a:map[string]int  
    

    map 支持在声明时填充元素:

    func main() {
        userInfo := map[string]string{
            "username": "pprof.cn",
            "password": "123456",
        }
        fmt.Println(userInfo)
    }
    
  3. 判断某个键是否存在

    Go 语言中有个特殊写法,判断 map 中的键是否存在:

    func main() {
        scoreMap := make(map[string]int)
        scoreMap["张三"] = 90
        scoreMap["小明"] = 100
        // 如果 key 存在 ok 为 true,v 为对应的值;不存在 ok 为 false,v 为值类型的零值
        v, ok := scoreMap["张三"]
        if ok {
            fmt.Println(v)
        } else {
            fmt.Println("查无此人")
        }
    } 
    
  4. 如何遍历 map

    Go 语言中使用 for range 遍历 map

    func main() {
        scoreMap := make(map[string]int)
        scoreMap["张三"] = 90
        scoreMap["小明"] = 100
        scoreMap["王五"] = 60
        for k, v := range scoreMap {
            fmt.Println(k, v)
        }
    }
    

    如果只想遍历 key,可以按下面的写法:

    func main() {
        scoreMap := make(map[string]int)
        scoreMap["张三"] = 90
        scoreMap["小明"] = 100
        scoreMap["王五"] = 60
        for k := range scoreMap {
            fmt.Println(k)
        }
    }
    

    注意:遍历 map 的元素顺序与添加键值对的顺序无关

  5. 使用 delete() 删除键值对

    func main(){
        scoreMap := make(map[string]int)
        scoreMap["张三"] = 90
        scoreMap["小明"] = 100
        scoreMap["王五"] = 60
        delete(scoreMap, "小明") // 将 "小明:100" 从 map 中删除
        for k,v := range scoreMap{
            fmt.Println(k, v)
        }
    } 
    
  6. 按指定顺序遍历 map(借助切片顺序)

    func main() {
        rand.Seed(time.Now().UnixNano()) // 初始化随机数种子
    
        var scoreMap = make(map[string]int, 200)
    
        for i := 0; i < 100; i++ {
            key := fmt.Sprintf("stu%02d", i) // 生成 stu 开头的字符串
            value := rand.Intn(100)          // 生成 0~99 的随机整数
            scoreMap[key] = value
        }
        // 取出 map 中的所有 key 存入切片 keys
        var keys = make([]string, 0, 200)
        for key := range scoreMap {
            keys = append(keys, key)
        }
        // 对切片进行排序
        sort.Strings(keys)
        // 按照排序后的 key 遍历 map
        for _, key := range keys {
            fmt.Println(key, scoreMap[key])
        }
    }
    
  7. 元素为 map 类型的切片

    func main() {
        var mapSlice = make([]map[string]string, 3)
        for index, value := range mapSlice {
            fmt.Printf("index:%d value:%v\n", index, value)
        }
        fmt.Println("after init")
        // 对切片中的 map 元素进行初始化
        mapSlice[0] = make(map[string]string, 10)
        mapSlice[0]["name"] = "王五"
        mapSlice[0]["password"] = "123456"
        mapSlice[0]["address"] = "红旗大街"
        for index, value := range mapSlice {
            fmt.Printf("index:%d value:%v\n", index, value)
        }
    } 
    
  8. 值为切片类型的 map

    func main() {
        var sliceMap = make(map[string][]string, 3)
        fmt.Println(sliceMap)
        fmt.Println("after init")
        key := "中国"
        value, ok := sliceMap[key]
        if !ok {
            value = make([]string, 0, 2)
        }
        value = append(value, "北京", "上海")
        sliceMap[key] = value
        fmt.Println(sliceMap)
    }
    

结构体

Go 语言中没有 “类” 的概念,也不支持 “类” 的继承等面向对象的概念。相对的,Go 语言通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性

自定义类型

Go 语言可以使用 type 关键字来定义自定义类型

自定义类型是定义了一个全新的类型,我们可以基于内置的基本类型定义,也可以通过 struct 定义

// 将 MyInt 定义为 int 类型
type MyInt int

通过 type 关键字的定义,MyInt 就是一种新的类型,它具有 int 的特性

类型别名

类型别名是 Go 1.9 版本添加的新功能

比如下面的代码:

type TypeAlias = Type

TypeAlias 只是 Type 的别名,本质上是同一个类型。之前提到过的 runebyte 就是类型别名:

type byte = uint8
type rune = int32

结构体

定义一个 Person 结构体

type person struct {
    name string
    city string
    age  int8
} 

同样类型的字段也可以写在同一行

type person struct {
    name, city string
    age  int8
} 

只有当结构体 实例化 时,才会真正地分配内存,也就是说我们必须实例化后才能使用结构体的字段。

结构体本身也是一种类型,我们可以像声明内置类型一样使用 var 声明结构体类型

var 结构体实例 结构体类型

基本实例化:

type person struct {
    name string
    city string
    age  int8
}

func main() {
    var p1 person
    p1.name = "pprof.cn"
    p1.city = "北京"
    p1.age = 18
    fmt.Printf("p1=%v\n", p1)  // p1={pprof.cn 北京 18}
    fmt.Printf("p1=%#v\n", p1) // p1=main.person{name:"pprof.cn", city:"北京", age:18}
} 

匿名结构体

定义一些临时数据结构时,还可以使用匿名结构体

func main() {
    var user struct{Name string; Age int}
    user.Name = "pprof.cn"
    user.Age = 18
    fmt.Printf("%#v\n", user)
} 

创建指针类型结构体

可以通过 new 关键字对结构体进行实例化,得到的是结构体的地址

var p2 = new(person)
fmt.Printf("%T\n", p2)     // *main.person
fmt.Printf("p2=%#v\n", p2) // p2=&main.person{name:"", city:"", age:0}

Go 语言支持对结构体指针的直接使用,来访问结构体的成员

var p2 = new(person)
p2.name = "测试"
p2.age = 18
p2.city = "北京"
fmt.Printf("p2=%#v\n", p2) // p2=&main.person{name:"测试", city:"北京", age:18}

取结构体的地址实例化

使用 & 对结构体进行取地址操作相当于对该结构体类型进行了一次 new 实例化操作

p3 := &person{}
fmt.Printf("%T\n", p3)     // *main.person
fmt.Printf("p3=%#v\n", p3) // p3=&main.person{name:"", city:"", age:0}
p3.name = "博客"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) // p3=&main.person{name:"博客", city:"成都", age:30}

p3.name = "博客" 其实在底层是 (*p3).name = "博客",这是 Go 语言的一种语法糖

结构体初始化

type person struct {
    name string
    city string
    age  int8
}

func main() {
    var p4 person
    fmt.Printf("p4=%#v\n", p4) // p4=main.person{name:"", city:"", age:0}
} 
p5 := person{
    name: "pprof.cn",
    city: "北京",
    age:  18,
}
fmt.Printf("p5=%#v\n", p5) // p5=main.person{name:"pprof.cn", city:"北京", age:18}

也可以用指针进行初始化

p6 := &person{
    name: "pprof.cn",
    city: "北京",
    age:  18,
}
fmt.Printf("p6=%#v\n", p6) // p6=&main.person{name:"pprof.cn", city:"北京", age:18}

当某些字段没有初始值时,该字段可以不写,此时没有指定初始值的字段就是该字段类型的零值

p7 := &person{
    city: "北京",
}
fmt.Printf("p7=%#v\n", p7) // p7=&main.person{name:"", city:"北京", age:0}

初始化结构体的时候可以简写,直接写值

p8 := &person{
    "pprof.cn",
    "北京",
    18,
}
fmt.Printf("p8=%#v\n", p8) // p8=&main.person{name:"pprof.cn", city:"北京", age:18}

使用这种格式初始化时,需要注意:

  1. 必须初始化结构体的所有字段
  2. 初始值的填充顺序必须与结构体中的声明顺序一致
  3. 该方式不能和键值初始化方式混用

构造函数

Go 语言的结构体没有构造函数,但可以自己实现

如果结构体比较复杂的话,值拷贝性能开销会比较大,所以构造函数返回的是结构体指针类型

func newPerson(name, city string, age int8) *person {
    return &person{
        name: name,
        city: city,
        age:  age,
    }
}

p9 := newPerson("pprof.cn", "测试", 90)
fmt.Printf("%#v\n", p9) 

方法和接收者

Go 语言中的 方法 是一种作用于特定类型变量的函数,这种特定变量叫做 接收者。接收者的概念就类似于其它语言中的 this 或者 self

举个例子:

//Person 结构体
type Person struct {
    name string
    age  int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
    return &Person{
        name: name,
        age:  age,
    }
}

func (p Person) Dream() {
    fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
    p1 := NewPerson("测试", 25)
    p1.Dream()
}

方法和函数的区别是:函数不属于任何类型,方法属于特定的类型

指针类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的 this 或者 self

// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
    p.age = newAge
}

func main() {
    p1 := NewPerson("测试", 25)
    fmt.Println(p1.age) // 25
    p1.SetAge(30)
    fmt.Println(p1.age) // 30
} 

值类型的接收者

当方法作用于值类型接收者时,Go 语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身

// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
    p.age = newAge
}

func main() {
    p1 := NewPerson("测试", 25)
    p1.Dream()
    fmt.Println(p1.age) // 25
    p1.SetAge2(30) // (*p1).SetAge2(30)
    fmt.Println(p1.age) // 25
} 

那么什么时候该使用指针类型的接收者呢?

  1. 需要修改接收者中的值
  2. 接收者是 拷贝代价比较大 的大对象
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者

结构体的匿名字段

结构体允许成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段

// Person 结构体Person类型
type Person struct {
    string
    int
}

func main() {
    p1 := Person{
        "pprof.cn",
        18,
    }
    fmt.Printf("%#v\n", p1)        // main.Person{string:"pprof.cn", int:18}
    fmt.Println(p1.string, p1.int) // pprof.cn 18
}  

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个

嵌套结构体

// Address 地址结构体
type Address struct {
    Province string
    City     string
}

// User 用户结构体
type User struct {
    Name    string
    Gender  string
    Address Address
}

func main() {
    user1 := User{
        Name:   "pprof",
        Gender: "女",
        Address: Address{
            Province: "黑龙江",
            City:     "哈尔滨",
        },
    }
    fmt.Printf("user1=%#v\n", user1) // user1=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}

嵌套匿名结构体

// Address 地址结构体
type Address struct {
    Province string
    City     string
}

// User 用户结构体
type User struct {
    Name    string
    Gender  string
    Address // 匿名结构体
}

func main() {
    var user2 User
    user2.Name = "pprof"
    user2.Gender = "女"
    user2.Address.Province = "黑龙江"    // 通过匿名结构体.字段名访问
    user2.City = "哈尔滨"                // 直接访问匿名结构体的字段名
    fmt.Printf("user2=%#v\n", user2) // user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}

如果嵌套结构体内部出现了相同的字段名,就需要指定具体的内嵌结构体的字段来避免歧义

// Address 地址结构体
type Address struct {
    Province   string
    City       string
    CreateTime string
}

// Email 邮箱结构体
type Email struct {
    Account    string
    CreateTime string
}

// User 用户结构体
type User struct {
    Name   string
    Gender string
    Address
    Email
}

func main() {
    var user3 User
    user3.Name = "pprof"
    user3.Gender = "女"
    // user3.CreateTime = "2019" // ambiguous selector user3.CreateTime
    user3.Address.CreateTime = "2000" // 指定 Address 结构体中的 CreateTime
    user3.Email.CreateTime = "2000"   // 指定 Email 结构体中的 CreateTime
}  

结构体的 “继承”

// Animal 动物
type Animal struct {
    name string
}

func (a *Animal) move() {
    fmt.Printf("%s会动!\n", a.name)
}

// Dog 狗
type Dog struct {
    Feet    int8
    *Animal // 通过嵌套匿名结构体实现继承
}

func (d *Dog) wang() {
    fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
    d1 := &Dog{
        Feet: 4,
        Animal: &Animal{ // 注意嵌套的是结构体指针
            name: "乐乐",
        },
    }
    d1.wang() // 乐乐会汪汪汪~
    d1.move() // 乐乐会动!
}