Go语言语法基础系统精讲

目录


go快速入门

Go语言简介

Go语言,通常称为Golang,是由Google设计的一种静态类型、编译型的编程语言。它旨在提高开发者的生产力,简化并加速软件开发流程。Go语言具有以下特点:

  • 简洁性:Go语言语法简单,易于学习,没有类如C++中的模板或Java中的泛型这样的复杂特性。
  • 并发支持:Go语言内置了对并发的支持,通过goroutines和channels来实现。
  • 标准库:Go拥有一个强大的标准库,提供了大量的工具和库来帮助开发者快速构建应用。
  • 编译速度快:Go语言的编译速度非常快,接近于C语言。
  • 跨平台:Go语言可以编译成各种不同的平台,包括Windows、Linux、macOS等。

Go语言快速入门

开发环境搭建

  • 安装Go: 下载并安装最新版本的Go语言环境。
  • 设置环境变量: 设置GOROOT为Go的安装路径,并将GOPATH(Go 1.11之前版本)或GO111MODULE(模块化支持)设置好。
  • 安装编辑器或IDE: 可以选择Visual Studio Code, IntelliJ IDEA, 或者GoLand等。

第一个Go程序
创建一个新的目录作为工作区,并在其中编写第一个Go程序:

package main

import "fmt"

func main() {
    fmt.Println("Hello, world!")
}

保存文件为hello.go,然后在命令行中运行:

go run hello.go

项目结构
一个典型的Go项目结构可能如下所示:

myproject/
├── cmd/
│   └── myapp/
│       ├── main.go
│       └── ...
├── internal/
│   ├── pkg1/
│   │   ├── pkg1.go
│   │   └── ...
│   └── pkg2/
│       ├── pkg2.go
│       └── ...
├── pkg/
│   ├── pkg3/
│   │   ├── pkg3.go
│   │   └── ...
│   └── pkg4/
│       ├── pkg4.go
│       └── ...
├── go.mod
└── go.sum
  • cmd/: 包含应用程序入口点,每个子目录代表一个可执行命令。
  • internal/: 存放内部包,这些包只能被项目内的其他包访问。
  • pkg/: 存放外部可以依赖的包。
  • go.mod: Go模块元数据文件,用于管理依赖关系。
  • go.sum: 文件包含直接和间接依赖的校验和,确保依赖的一致性和安全性。

构建与测试
你可以通过以下命令来构建你的Go应用:

go build -o myapp ./cmd/myapp

对于测试,Go的标准库提供了一个简单的测试框架,可以通过下面的命令来运行测试:

go test ./...

go基本概念

Go语言的主要特征:

  1. 自动立即回收。
  2. 更丰富的内置类型。
  3. 函数多返回值。
  4. 错误处理。
  5. 匿名函数和闭包。
  6. 类型和接口。
  7. 并发编程。
  8. 反射。
  9. 语言交互性。

Go语言命名:

  • Go的函数、变量、常量、自定义类型、包(package)的命名方式遵循以下规则:

    1. 首字符可以是任意的Unicode字符或者下划线
    2. 剩余字符可以是Unicode字符、下划线、数字
    3. 字符长度不限
  • Go只有25个关键字
    break、default、func、interface、select、case、defer、go、map、struct、chan、else、goto、package、switch、const、fallthrough、if 、range、ype、continue、for、import、return、var

  • Go还有37个保留字

    Constants: true false iota nil

    Types: int int8 int16 int32 int64
    uint uint8 uint16 uint32 uint64 uintptr
    float32 float64 complex128 complex64
    bool byte rune string error

    Functions: make len cap new append copy close delete
    complex real imag
    panic recover

  • 可见性:

    1. 声明在函数内部,是函数的本地值,类似private
    2. 声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect
    3. 声明在函数外部且首字母大写是所有包可见的全局值,类似public

Go语言声明:

  • 有四种主要声明方式:

    var(声明变量), const(声明常量), type(声明类型) ,func(声明函数)。

Go 值类型:

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

Go 引用类型:(指针类型)

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

Go 内置函数

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

init 函数

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

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

init 函数和 main 函数的异同

相同点:

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

不同点:

- init可以应用于任意包中,且可以重复定义多个。
- main函数只能用于main包中,且只能定义一个。

两个函数的执行顺序:

  • 对同一个go文件的init()调用顺序是从上到下的。

  • 对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数。

  • 对于不同的package,如果不相互依赖的话,按照main包中”先import的后调用”的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。

go命令

  • go env用于打印Go语言的环境信息。
  • go run命令可以编译并运行命令源码文件。
  • go get可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。
  • go build命令用于编译我们指定的源码文件或代码包以及它们的依赖包。
  • go install用于编译并安装指定的代码包及它们的依赖包。
  • go clean命令会删除掉执行其它命令时产生的一些文件和目录。
  • go doc命令可以打印附于Go语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的。
  • go test命令用于对Go语言编写的程序进行测试。
  • go list命令的作用是列出指定的代码包的信息。
  • go fix会把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码。
  • go vet是一个用于检查Go语言源码中静态错误的简单工具。
  • go tool pprof命令来交互式的访问概要文件的内容。

基本类型

整型

整型分为以下两个大类: 按长度分为:int8int16int32int64对应的无符号整型:uint8uint16uint32uint64

其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

浮点型

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

复数

complex64complex128

复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。

布尔值

Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)和false(假)两个值。

字符串

Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(intboolfloat32float64 等)一样。 Go 语言里的字符串的内部实现使用UTF-8编码。 字符串的值为双引号(“)中的内容,可以在Go语言的源码中直接添加非ASCII码字符,

字符串转义符

  • \r 回车符(返回行首)
  • \n 换行符(直接跳到下一行的同列位置)
  • \t 制表符
  • ’ 单引号
  • " 双引号
  • \ 反斜杠

多行字符串

Go语言中要定义一个多行字符串时,就必须使用反引号字符:

 s1 := `第一行
  第二行
  第三行
  `
  fmt.Println(s1)

字符串的常用操作

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

byte和rune类型

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()
  }

修改字符串

要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。

 func changeString() {
    s1 := "hello"
    // 强制类型转换
    byteS1 := []byte(s1)
    byteS1[0] = 'H'
    fmt.Println(string(byteS1))

    s2 := "博客"
    runeS2 := []rune(s2)
    runeS2[0] = '狗'
    fmt.Println(string(runeS2))
    }

类型转换

强制类型转换的基本语法如下:T(表达式)其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等.

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)
}

常量变量运算符

常量

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

const pi = 3.1415
const e = 2.7182

// 多个常量也可以一起声明
const (
  pi = 3.1415
  e = 2.7182
)
const (
  n1 = 100
  n2
  n3
)

// iota是go语言的常量计数器,只能在常量的表达式中使用。iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。
const (
  n1 = iota //0
  n2        //1
  _
  n4        //3
)
const (
  n1 = iota //0
  n2 = 100  //100
  n3 = iota //2
  n4        //3
)

变量

var 变量名 变量类型; var 变量名 类型 = 表达式

var name string
var age int
var isOk bool
var name string = "pprof.cn"
var sex int = 1
var name, sex = "pprof.cn", 1
func main() {
  n := 10
  m := 200 // 此处声明局部变量m
  fmt.Println(m, n)

  x, _ := foo()
  _, y := foo()  // 匿名变量
}
func foo() (int, string) {
  return 10, "Q1mi"
}

运算符

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

数组

Go 数组特征

  1. 数组:是同一种数据类型的固定长度的序列。
  2. 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
  3. 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。
  4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
    for i := 0; i < len(a); i++ {
    }
    for index, v := range a {
    }
  5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
  6. 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
  7. 支持 “==”、“!=” 操作符,因为内存总是被初始化过的。
  8. 指针数组 [n]*T,数组指针 *[n]T。

一维数组

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)
}

多维数组

package main
import (
    "fmt"
)
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

func main() {
    a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
    b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
    fmt.Println(arr0, arr1)
    fmt.Println(a, b)
}

多维数组遍历

package main
import (
    "fmt"
)
func main() {

    var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

    for k1, v1 := range f {
        for k2, v2 := range v1 {
            fmt.Printf("(%d,%d)=%d ", k1, k2, v2)
        }
        fmt.Println()
    }
}

数组拷贝和传参

package main
import "fmt"
func printArr(arr *[5]int) {
    arr[0] = 10
    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)
}

切片Slice

切片Slice特征

  1. 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
  2. 切片的长度可以改变,因此,切片是一个可变的数组。
  3. 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。
  4. cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
  5. 切片的定义:var 变量名 []类型,比如 var str []string var arr []int。
  6. 如果 slice == nil,那么 len、cap 结果都等于 0。

创建切片的各种方式

package main

import "fmt"

func main() {
   //1.声明切片
   var s1 []int
   if s1 == nil {
      fmt.Println("是空")
   } else {
      fmt.Println("不是空")
   }
   // 2.:=
   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)
}

切片初始化

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)
}

通过make来创建切片

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

// 实例
package main
import (
    "fmt"
)

var slice0 []int = make([]int, 10)
var slice1 = make([]int, 10)
var slice2 = make([]int, 10, 10)

func main() {
    fmt.Printf("make全局slice0 :%v\n", slice0)
    fmt.Printf("make全局slice1 :%v\n", slice1)
    fmt.Printf("make全局slice2 :%v\n", slice2)
    fmt.Println("--------------------------------------")
    slice3 := make([]int, 10)
    slice4 := make([]int, 10)
    slice5 := make([]int, 10, 10) // 使用 make 创建,指定 len 和 cap 值。

    s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
    fmt.Println(s2, len(s2), cap(s2))
    s3 := make([]int, 6) // 省略 cap,相当于 cap = len。

    fmt.Printf("make局部slice3 :%v\n", slice3)
    fmt.Printf("make局部slice4 :%v\n", slice4)
    fmt.Printf("make局部slice5 :%v\n", slice5)
}

用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)

  s1 := make([]int, 0, 5)
  fmt.Printf("%p\n", &s1)

  s2 := append(s1, 1)  // 向 slice 尾部添加数据,返回新的 slice 对象。
  fmt.Printf("%p\n", &s2)

  fmt.Println(s1, s2)
}

超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满。

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]) // 比对底层数组起始指针。

    m := make([]int, 0, 1)
    c := cap(m)

    for i := 0; i < 50; i++ {
        m = append(m, i)
        if n := cap(m); n > c {
            fmt.Printf("cap: %d -> %d\n", c, n)
            c = n
        }
    }
}

切片拷贝

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遍历

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)
    }

}

切片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))
}

字符串和切片(string and slice)

package main

import (
    "fmt"
)

func main() {
    str := "hello world"
    s1 := str[0:5]
    fmt.Println(s1)

    s2 := str[6:]
    fmt.Println(s2)

    s := []byte(str) //中文字符需要用[]rune(str)
    s[6] = 'G'
    s = s[:8]
    s = append(s, '!')
    str = string(s)
    fmt.Println(str)
}

Slice实现原理

切片是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。切片常见的操作有 reslice、append、copy。与此同时,切片还具有可索引,可迭代的优秀特性。

切片和数组

用切片传数组参数,既可以达到节约内存的目的,也可以达到合理处理好共享内存的问题。切片的指针和原来数组的指针是不同的。切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。并非所有时候都适合用切片代替数组,因为切片底层数组可能会在堆上分配内存,而且小数组在栈上拷贝的消耗也未必比make 消耗大。

package main
import "testing"
func array() [1024]int {
    var x [1024]int
    for i := 0; i < len(x); i++ {
        x[i] = i
    }
    return x
}
func slice() []int {
    x := make([]int, 1024)
    for i := 0; i < len(x); i++ {
        x[i] = i
    }
    return x
}
func BenchmarkArray(b *testing.B) {
    for i := 0; i < b.N; i++ {
        array()
    }
}
func BenchmarkSlice(b *testing.B) {
    for i := 0; i < b.N; i++ {
        slice()
    }
}

切片的数据结构

type slice struct {
  array unsafe.Pointer
  len   int
  cap   int
}

切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。

// 从 slice 中得到一块内存地址
s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])

// 从 Go 的内存地址中构造一个 slice
var ptr unsafe.Pointer
var s1 = struct {
    addr uintptr
    len int
    cap int
}{ptr, length, length}
s := *(*[]byte)(unsafe.Pointer(&s1))

// 在 Go 的反射中就存在一个与之对应的数据结构 SliceHeader,我们可以用它来构造一个 slice
var o []byte
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o)))
sliceHeader.Cap = length
sliceHeader.Len = length
sliceHeader.Data = uintptr(ptr)

make 和切片字面量

func makeslice(et *_type, len, cap int) slice {
  // 根据切片的数据类型,获取切片的最大容量
  maxElements := maxSliceCap(et.size)
  // 比较切片的长度,长度值域应该在[0,maxElements]之间
  if len < 0 || uintptr(len) > maxElements {
      panic(errorString("makeslice: len out of range"))
  }
  // 比较切片的容量,容量值域应该在[len,maxElements]之间
  if cap < len || uintptr(cap) > maxElements {
      panic(errorString("makeslice: cap out of range"))
  }
  // 根据切片的容量申请内存
  p := mallocgc(et.size*uintptr(cap), et, true)
  // 返回申请好内存的切片的首地址
  return slice{p, len, cap}
}

func makeslice64(et *_type, len64, cap64 int64) slice {
  len := int(len64)
  if int64(len) != len64 {
      panic(errorString("makeslice: len out of range"))
  }

  cap := int(cap64)
  if int64(cap) != cap64 {
      panic(errorString("makeslice: cap out of range"))
  }

  return makeslice(et, len, cap)
}

切片扩容

func growslice(et *_type, old slice, cap int) slice {
  if raceenabled {
      callerpc := getcallerpc(unsafe.Pointer(&et))
      racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
  }
  if msanenabled {
      msanread(old.array, uintptr(old.len*int(et.size)))
  }

  if et.size == 0 {
      // 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。
      if cap < old.cap {
          panic(errorString("growslice: cap out of range"))
      }

      // 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。
      return slice{unsafe.Pointer(&zerobase), old.len, cap}
  }

// 这里就是扩容的策略
  newcap := old.cap
  doublecap := newcap + newcap
  if cap > doublecap {
      newcap = cap
  } else {
      if old.len < 1024 {
          newcap = doublecap
      } else {
          for newcap < cap {
              newcap += newcap / 4
          }
      }
  }

  // 计算新的切片的容量,长度。
  var lenmem, newlenmem, capmem uintptr
  const ptrSize = unsafe.Sizeof((*byte)(nil))
  switch et.size {
  case 1:
      lenmem = uintptr(old.len)
      newlenmem = uintptr(cap)
      capmem = roundupsize(uintptr(newcap))
      newcap = int(capmem)
  case ptrSize:
      lenmem = uintptr(old.len) * ptrSize
      newlenmem = uintptr(cap) * ptrSize
      capmem = roundupsize(uintptr(newcap) * ptrSize)
      newcap = int(capmem / ptrSize)
  default:
      lenmem = uintptr(old.len) * et.size
      newlenmem = uintptr(cap) * et.size
      capmem = roundupsize(uintptr(newcap) * et.size)
      newcap = int(capmem / et.size)
  }

  // 判断非法的值,保证容量是在增加,并且容量不超过最大容量
  if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
      panic(errorString("growslice: cap out of range"))
  }

  var p unsafe.Pointer
  if et.kind&kindNoPointers != 0 {
      // 在老的切片后面继续扩充容量
      p = mallocgc(capmem, nil, false)
      // 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处
      memmove(p, old.array, lenmem)
      // 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。
      memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
  } else {
      // 重新申请新的数组给新切片
      // 重新申请 capmen 这个大的内存地址,并且初始化为0值
      p = mallocgc(capmem, et, true)
      if !writeBarrier.enabled {
          // 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处
          memmove(p, old.array, lenmem)
      } else {
          // 循环拷贝老的切片的值
          for i := uintptr(0); i < lenmem; i += et.size {
              typedmemmove(et, add(p, i), add(old.array, i))
          }
      }
  }
  // 返回最终新切片,容量更新为最新扩容之后的容量
  return slice{p, old.len, newcap}
}

切片拷贝

func slicecopy(to, fm slice, width uintptr) int {
  // 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return 
  if fm.len == 0 || to.len == 0 {
      return 0
  }
  // n 记录下源切片或者目标切片较短的那一个的长度
  n := fm.len
  if to.len < n {
      n = to.len
  }
  // 如果入参 width = 0,也不需要拷贝了,返回较短的切片的长度
  if width == 0 {
      return n
  }
  // 如果开启了竞争检测
  if raceenabled {
      callerpc := getcallerpc(unsafe.Pointer(&to))
      pc := funcPC(slicecopy)
      racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
      racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
  }
  // 如果开启了 The memory sanitizer (msan)
  if msanenabled {
      msanwrite(to.array, uintptr(n*int(width)))
      msanread(fm.array, uintptr(n*int(width)))
  }

  size := uintptr(n) * width
  if size == 1 { 
      // TODO: is this still worth it with new memmove impl?
      // 如果只有一个元素,那么指针直接转换即可
      *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
  } else {
      // 如果不止一个元素,那么就把 size 个 bytes 从 fm.array 地址开始,拷贝到 to.array 地址之后
      memmove(to.array, fm.array, size)
  }
  return n
}

还有一个拷贝的方法,这个方法原理和 slicecopy 方法类似

func slicestringcopy(to []byte, fm string) int {
  // 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return 
  if len(fm) == 0 || len(to) == 0 {
      return 0
  }
  // n 记录下源切片或者目标切片较短的那一个的长度
  n := len(fm)
  if len(to) < n {
      n = len(to)
  }
  // 如果开启了竞争检测
  if raceenabled {
      callerpc := getcallerpc(unsafe.Pointer(&to))
      pc := funcPC(slicestringcopy)
      racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
  }
  // 如果开启了 The memory sanitizer (msan)
  if msanenabled {
      msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
  }
  // 拷贝字符串至字节数组
  memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
  return n
}

Map

map定义

Go语言中 map的定义语法如: map[KeyType]ValueType, KeyType:表示键的类型。 ValueType:表示键对应的值的类型。

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:make(map[KeyType]ValueType, [cap]), 其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

map基本使用

map中的数据都是成对出现的,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)

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

Go语言中有个判断map中键是否存在的特殊写法,格式如下:
value, ok := map[key]

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("查无此人")
  }
}  
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)
  }
}  
使用delete()函数删除键值对

使用delete()内建函数从map中删除一组键值对,delete()函数的格式如下:

delete(map, key)

其中,

  • map:表示要删除键值对的map
  • key:表示要删除的键值对的键
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)
  }
}  
按照指定顺序遍历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])
  }
}
元素为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)
  }
} 
值为切片类型的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)
}

Map实现原理

Map是一种通过key来获取value的一个数据结构,其底层存储方式为数组,在存储时key不能重复,当key重复时,value进行覆盖,我们通过key进行hash运算(可以简单理解为把key转化为一个整形数字)然后对数组的长度取余,得到key存储在数组的哪个下标位置,最后将key和value组装为一个结构体,放入数组下标处

  length = len(array) = 4
  hashkey1 = hash(xiaoming) = 4
  index1  = hashkey1% length= 0
  hashkey2 = hash(xiaoli) = 6
  index2  = hashkey2% length= 2

Go中Map的使用

//直接创建初始化一个mao
var mapInit = map[string]string {"xiaoli":"湖南", "xiaoliu":"天津"}
//声明一个map类型变量,
//map的key的类型是string,value的类型是string
var mapTemp map[string]string
//使用make函数初始化这个变量,并指定大小(也可以不指定)
mapTemp = make(map[string]string,10)
//存储key ,value
mapTemp["xiaoming"] = "北京"
mapTemp["xiaowang"]= "河北"
//根据key获取value,
//如果key存在,则ok是true,否则是flase
//v1用来接收key对应的value,当ok是false时,v1是nil
v1,ok := mapTemp["xiaoming"]
fmt.Println(ok,v1)
//当key=xiaowang存在时打印value
if v2,ok := mapTemp["xiaowang"]; ok{
    fmt.Println(v2)
}
//遍历map,打印key和value
for k,v := range mapTemp{
    fmt.Println(k,v)
}
//删除map中的key
delete(mapTemp,"xiaoming")
//获取map的大小
l := len(mapTemp)
fmt.Println(l)

Go中Map的实现原理

go底层map到底怎么存储呢?接下来我们一探究竟。map的源码位于 src/runtime/map.go中 笔者go的版本是1.12在go中,map同样也是数组存储的的,每个数组下标处存储的是一个bucket,这个bucket的类型见下面代码,每个bucket中可以存储8个kv键值对,当每个bucket存储的kv对到达8个之后,会通过overflow指针指向一个新的bucket,从而形成一个链表,看bmap的结构,我想大家应该很纳闷,没看见kv的结构和overflow指针啊,事实上,这两个结构体并没有显示定义,是通过指针运算进行访问的。

go的整体内存结构,阅读一下map存储的源码,当往map中存储一个kv对时,通过k获取hash值,hash值的低八位和bucket数组长度取余,定位到在数组中的那个下标,hash值的高八位存储在bucket中的tophash中,用来快速判断key是否存在,key和value的具体值则通过指针运算存储,当一个bucket满时,通过overfolw指针链接到下一个bucket。

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    //获取hash算法
    alg := t.key.alg
    //计算hash值
    hash := alg.hash(key, uintptr(h.hash0))
    //如果bucket数组一开始为空,则初始化
    if h.buckets == nil {
        h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
    }
again:
    // 定位存储在哪一个bucket中
    bucket := hash & bucketMask(h.B)
    //得到bucket的结构体
    b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) +bucket*uintptr(t.bucketsize)))
    //获取高八位hash值
    top := tophash(hash)
    var inserti *uint8
    var insertk unsafe.Pointer
    var val unsafe.Pointer
bucketloop:
    //死循环
    for {
        //循环bucket中的tophash数组
        for i := uintptr(0); i < bucketCnt; i++ {
            //如果hash不相等
            if b.tophash[i] != top {
             //判断是否为空,为空则插入
                if isEmpty(b.tophash[i]) && inserti == nil {
                    inserti = &b.tophash[i]
                    insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
                    val = add( unsafe.Pointer(b), 
                    dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize) )
                }
              //插入成功,终止最外层循环
                if b.tophash[i] == emptyRest {
                    break bucketloop
                }
                continue
            }
            //到这里说明高八位hash一样,获取已存在的key
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            //判断两个key是否相等,不相等就循环下一个
            if !alg.equal(key, k) {
                continue
            }
            // 如果相等则更新
            if t.needkeyupdate() {
                typedmemmove(t.key, k, key)
            }
            //获取已存在的value
            val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
            goto done
        }
        //如果上一个bucket没能插入,则通过overflow获取链表上的下一个bucket
        ovf := b.overflow(t)
        if ovf == nil {
            break
        }
        b = ovf
    }

    if inserti == nil {
        // all current buckets are full, allocate a new one.
        newb := h.newoverflow(t, b)
        inserti = &newb.tophash[0]
        insertk = add(unsafe.Pointer(newb), dataOffset)
        val = add(insertk, bucketCnt*uintptr(t.keysize))
    }

    // store new key/value at insert position
    if t.indirectkey() {
        kmem := newobject(t.key)
        *(*unsafe.Pointer)(insertk) = kmem
        insertk = kmem
    }
    if t.indirectvalue() {
        vmem := newobject(t.elem)
        *(*unsafe.Pointer)(val) = vmem
    }
    typedmemmove(t.key, insertk, key)
    //将高八位hash值存储
    *inserti = top
    h.count++
    return val
}

指针

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(intfloatboolstringarraystruct)都有对应的指针类型,如:*int*int64*string等。

取变量指针的语法如下:

ptr := &v // v的类型为T

其中:

v:代表被取地址的变量,类型为T
ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。

func main() {
  a := 10
  b := &a
  fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
  fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
  fmt.Println(&b)                    // 0xc00000e018
}

指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,

func main() {
    //指针取值
    a := 10
    b := &a // 取变量a的地址,将指针保存到b中
    fmt.Printf("type of b:%T\n", b)
    c := *b // 指针取值(根据指针去内存取值)
    fmt.Printf("type of c:%T\n", c)
    fmt.Printf("value of c:%v\n", c)
}

// 指针传值示例:
func modify1(x int) {
    x = 100
}

func modify2(x *int) {
    *x = 100
}

func main() {
    a := 10
    modify1(a)
    fmt.Println(a) // 10
    modify2(&a)
    fmt.Println(a) // 100
}

总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:\

  1. 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
  2. 指针变量的值是指针地址。
  3. 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

空指针

  • 当一个指针被定义后没有分配到任何变量时,它的值为 nil
  • 空指针的判断
package main

import "fmt"

func main() {
    var p *string
    fmt.Println(p)
    fmt.Printf("p的值是%v\n", p)
    if p != nil {
        fmt.Println("非空")
    } else {
        fmt.Println("空值")
    }
}

new和make

Go语言中new和make是内建的两个函数,主要用来分配内存

func main() {
  var a *int
  *a = 100
  fmt.Println(*a)

  var b map[string]int
  b["测试"] = 100
  fmt.Println(b)
}

new是一个内置的函数,它的函数签名如下:

func new(Type) *Type

其中,

  1. Type表示类型,new函数只接受一个参数,这个参数是一个类型
  2. *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值

func main() {
  a := new(int)
  b := new(bool)
  fmt.Printf("%T\n", a) // *int
  fmt.Printf("%T\n", b) // *bool
  fmt.Println(*a)       // 0
  fmt.Println(*b)       // false
} 

make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:

func make(t Type, size ...IntegerType) Type

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。这个我们在上一章中都有说明,关于channel我们会在后续的章节详细说明。

本节开始的示例中var b map[string]int只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:

func main() {
  var b map[string]int
  b = make(map[string]int, 10)
  b["测试"] = 100
  fmt.Println(b)
} 

new与make的区别

  1. 二者都是用来做内存分配的。
  2. make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
  3. 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

结构体

类型别名和自定义类型

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

type TypeAlias = Type 
type byte = uint8
type rune = int32

结构体的定义和实例化

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}

  // 匿名结构体
  var user struct{Name string; Age int}
  user.Name = "pprof.cn"
  user.Age = 18
  fmt.Printf("%#v\n", user)

  // 创建指针类型结构体
  var p2 = new(person)
  p2.name = "测试"
  p2.age = 18
  p2.city = "北京"
  fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"测试", city:"北京", age:18} 

  // 取结构体的地址实例化
  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}

  // 结构体初始化
  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} 

  // 使用值的列表初始化
  p8 := &person{
    "pprof.cn",
    "北京",
    18,
  }
  fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"pprof.cn", city:"北京", age:18}
} 

结构体内存布局

type test struct {
  a int8
  b int8
  c int8
  d int8
}
n := test{
  1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d) 

构造函数

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语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。

方法的定义格式如下:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}

其中,

  1. 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
  2. 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
  3. 方法名、参数列表、返回参数:具体格式与函数定义相同。
//Person 结构体
type Person struct {
    name string
    age  int8
}

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

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

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

指针类型的接收者

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

// 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
} 

任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

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

//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
  fmt.Println("Hello, 我是一个int。")
}
func main() {
  var m1 MyInt
  m1.SayHello() //Hello, 我是一个int。
  m1 = 100
  fmt.Printf("%#v  %T\n", m1, m1) //100  main.MyInt
} 

结构体的匿名字段

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

//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:"哈尔滨"}}
} 

结构体的“继承”

//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() //乐乐会动!
}

结构体的“继承”

结构体与JSON序列化

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号””包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。

//Student 学生
type Student struct {
  ID     int
  Gender string
  Name   string
}

//Class 班级
type Class struct {
  Title    string
  Students []*Student
}

func main() {
  c := &Class{
      Title:    "101",
      Students: make([]*Student, 0, 200),
  }
  for i := 0; i < 10; i++ {
      stu := &Student{
          Name:   fmt.Sprintf("stu%02d", i),
          Gender: "男",
          ID:     i,
      }
      c.Students = append(c.Students, stu)
  }
  //JSON序列化:结构体-->JSON格式的字符串
  data, err := json.Marshal(c)
  if err != nil {
      fmt.Println("json marshal failed")
      return
  }
  fmt.Printf("json:%s\n", data)
  //JSON反序列化:JSON格式的字符串-->结构体
  str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
  c1 := &Class{}
  err = json.Unmarshal([]byte(str), c1)
  if err != nil {
      fmt.Println("json unmarshal failed!")
      return
  }
  fmt.Printf("%#v\n", c1)
} 

结构体标签(Tag)

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

//Student 学生
type Student struct {
  ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
  Gender string //json序列化是默认使用字段名作为key
  name   string //私有不能被json包访问
}

func main() {
  s1 := Student{
      ID:     1,
      Gender: "女",
      name:   "pprof",
  }
  data, err := json.Marshal(s1)
  if err != nil {
      fmt.Println("json marshal failed!")
      return
  }
  fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
} 

删除map类型的结构体

package main

import "fmt"

type student struct {
    id   int
    name string
    age  int
}

func main() {
    ce := make(map[int]student)
    ce[1] = student{1, "xiaolizi", 22}
    ce[2] = student{2, "wang", 23}
    fmt.Println(ce)
    delete(ce, 2)
    fmt.Println(ce)
}

实现map有序输出

package main

import (
    "fmt"
    "sort"
)

func main() {
    map1 := make(map[int]string, 5)
    map1[1] = "www.topgoer.com"
    map1[2] = "rpc.topgoer.com"
    map1[5] = "ceshi"
    map1[3] = "xiaohong"
    map1[4] = "xiaohuang"
    sli := []int{}
    for k, _ := range map1 {
        sli = append(sli, k)
    }
    sort.Ints(sli)
    for i := 0; i < len(map1); i++ {
        fmt.Println(map1[sli[i]])
    }
}

流程控制

// if
package main

import "fmt"

func main() {
   /* 局部变量定义 */
   var a int = 100
   /* 判断布尔表达式 */
   if a < 20 {
       /* 如果条件为 true 则执行以下语句 */
       fmt.Printf("a 小于 20\n" )
   } else {
       /* 如果条件为 false 则执行以下语句 */
       fmt.Printf("a 不小于 20\n" )
   }
   fmt.Printf("a 的值为 : %d\n", a)
}

// switch 语句
package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var grade string = "B"
   var marks int = 90

   switch marks {
      case 90: grade = "A"
      case 80: grade = "B"
      case 50,60,70 : grade = "C"
      default: grade = "D"  
   }

   switch {
      case grade == "A" :
         fmt.Printf("优秀!\n" )     
      case grade == "B", grade == "C" :
         fmt.Printf("良好\n" )      
      case grade == "D" :
         fmt.Printf("及格\n" )      
      case grade == "F":
         fmt.Printf("不及格\n" )
      default:
         fmt.Printf("差\n" )
   }
   fmt.Printf("你的等级是 %s\n", grade )
}

// select 语句
/**
  select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

  select 是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。
  select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
*/
package main

import "fmt"

func main() {
   var c1, c2, c3 chan int
   var i1, i2 int
   select {
      case i1 = <-c1:
         fmt.Printf("received ", i1, " from c1\n")
      case c2 <- i2:
         fmt.Printf("sent ", i2, " to c2\n")
      case i3, ok := (<-c3):  // same as: i3, ok := <-c3
         if ok {
            fmt.Printf("received ", i3, " from c3\n")
         } else {
            fmt.Printf("c3 is closed\n")
         }
      default:
         fmt.Printf("no communication\n")
   }    
} 

//比如在下面的场景中,使用全局resChan来接受response,如果时间超过3S,resChan中还没有数据返回,则第二条case将执行
var resChan = make(chan int)
// do request
func test() {
    select {
    case data := <-resChan:
        doData(data)
    case <-time.After(time.Second * 3):
        fmt.Println("request time out")
    }
}

func doData(data int) {
    //...
}

// Golang for支持三种循环方式,包括类似 while 的语法。
package main
import "fmt"
func main() {

   var b int = 15
   var a int

   numbers := [6]int{1, 2, 3, 5}

   /* for 循环 */
   for a := 0; a < 10; a++ {
      fmt.Printf("a 的值为: %d\n", a)
   }

   for a < b {
      a++
      fmt.Printf("a 的值为: %d\n", a)
      }

   for i,x:= range numbers {
      fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
   }   
} 

// 循环语句range
package main
import "fmt"
func main() {
  a := [3]int{0, 1, 2}

  for i, v := range a { // index、value 都是从复制品中取出。
    if i == 0 { // 在修改前,我们先修改原数组。
        a[1], a[2] = 999, 999
        fmt.Println(a) // 确认修改有效,输出 [0, 999, 999]。
    }

    a[i] = v + 100 // 使用复制品中取出的 value 修改原数组。
  }
  fmt.Println(a) // 输出 [100, 101, 102]。
}

// 循环控制Goto、Break、Continue
// 1. 三个语句都可以配合标签(label)使用
// 2. 标签名区分大小写,定以后若不使用会造成编译错误
// 3. `continue`、`break`配合标签(label)可用于多层循环跳出
// 4. `goto`是调整执行位置,与`continue`、`break`配合标签(label)的结果并不相同  

时间和日期处理

时间和日期的基本操作

获取当前时间

import "time"

now := time.Now()
fmt.Println(now)

时间格式化

formattedTime := now.Format("2006-01-02 15:04:05")
fmt.Println(formattedTime)

解析时间

parsedTime, err := time.Parse("2006-01-02 15:04:05", "2023-08-29 12:34:56")
if err != nil {
    fmt.Println("Error parsing time:", err)
} else {
    fmt.Println(parsedTime)
}

时间间隔

t1 := time.Date(2023, 8, 29, 12, 0, 0, 0, time.UTC)
t2 := time.Date(2023, 8, 29, 13, 0, 0, 0, time.UTC)
duration := t2.Sub(t1)
fmt.Println("Duration:", duration)

时间加减

oneHour := time.Hour
futureTime := now.Add(oneHour)
pastTime := now.Add(-oneHour)
fmt.Println("Future time:", futureTime)
fmt.Println("Past time:", pastTime)

时间戳

timestamp := now.Unix()
fmt.Println("Timestamp:", timestamp)

时间本地化

loc, _ := time.LoadLocation("Asia/Shanghai")
localTime := now.In(loc)
fmt.Println("Local time:", localTime)

示例代码
下面是一个完整的示例代码,展示了上述基本操作:

package main

import (
    "fmt"
    "time"
)

func main() {
    // 获取当前时间
    now := time.Now()
    fmt.Println("Current time:", now)

    // 时间格式化
    formattedTime := now.Format("2006-01-02 15:04:05")
    fmt.Println("Formatted time:", formattedTime)

    // 解析时间
    parsedTime, err := time.Parse("2006-01-02 15:04:05", "2023-08-29 12:34:56")
    if err != nil {
        fmt.Println("Error parsing time:", err)
    } else {
        fmt.Println("Parsed time:", parsedTime)
    }

    // 时间间隔
    t1 := time.Date(2023, 8, 29, 12, 0, 0, 0, time.UTC)
    t2 := time.Date(2023, 8, 29, 13, 0, 0, 0, time.UTC)
    duration := t2.Sub(t1)
    fmt.Println("Duration:", duration)

    // 时间加减
    oneHour := time.Hour
    futureTime := now.Add(oneHour)
    pastTime := now.Add(-oneHour)
    fmt.Println("Future time:", futureTime)
    fmt.Println("Past time:", pastTime)

    // 时间戳
    timestamp := now.Unix()
    fmt.Println("Timestamp:", timestamp)

    // 时间本地化
    loc, _ := time.LoadLocation("Asia/Shanghai")
    localTime := now.In(loc)
    fmt.Println("Local time:", localTime)
}

深入理解nil

在Go语言中,nil 是一个特殊值,表示“没有值”或“空引用”。它主要用于指针、接口、函数、切片、映射、通道等类型。

nil 的常见用途

指针

var p *int
fmt.Println(p) // 输出: <nil>

切片

var s []int
fmt.Println(s) // 输出: []
fmt.Println(s == nil) // 输出: true

映射

var m map[string]int
fmt.Println(m) // 输出: map[]
fmt.Println(m == nil) // 输出: true

通道

var c chan int
fmt.Println(c) // 输出: <nil>

函数

func add(a, b int) int {
    return a + b
}

var f func(int, int) int
fmt.Println(f == nil) // 输出: true

接口

type MyInterface interface {
    DoSomething()
}

var i MyInterface
fmt.Println(i == nil) // 输出: true

nil 的检测

指针

var p *int
if p == nil {
    fmt.Println("p is nil")
}

切片

var s []int
if s == nil {
    fmt.Println("s is nil")
}

映射

var m map[string]int
if m == nil {
    fmt.Println("m is nil")
}

通道

var c chan int
if c == nil {
    fmt.Println("c is nil")
}

函数

var f func(int, int) int
if f == nil {
    fmt.Println("f is nil")
}

接口

var i MyInterface
if i == nil {
    fmt.Println("i is nil")
}

示例代码
下面是一个完整的示例代码,展示了如何使用和检测 nil:

package main

import (
    "fmt"
)

type MyInterface interface {
    DoSomething()
}

func add(a, b int) int {
    return a + b
}

func main() {
    // 指针
    var p *int
    fmt.Println("Pointer p:", p)

    // 切片
    var s []int
    fmt.Println("Slice s:", s)

    // 映射
    var m map[string]int
    fmt.Println("Map m:", m)

    // 通道
    var c chan int
    fmt.Println("Channel c:", c)

    // 函数
    var f func(int, int) int
    fmt.Println("Function f:", f)

    // 接口
    var i MyInterface
    fmt.Println("Interface i:", i)
}

// 输出:
// Pointer p: <nil>
// Slice s: []
// Map m: map[]
// Channel c: <nil>
// Function f: <nil>
// Interface i: <nil>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天涯学馆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值