Golang基础

Golang基础

第一个go程序

新建一个文件helloworld.go

package main // 声明包名main

import "fmt" // 导入fmt标准库包

func main() { // 定义main函数
    fmt.Println("hello world") // 调用fmt包的Println方法打印字符串到控制台
}

使用go run helloworld.go 直接编译运行

liuyi@DESKTOP-O2GI8R6 MINGW64 /d/工作学习/Go
$ go run helloword.go
hello world

或者先编译成二进制文件,再执行二进制文件

liuyi@DESKTOP-O2GI8R6 MINGW64 /d/工作学习/Go
$ go build helloword.go 

liuyi@DESKTOP-O2GI8R6 MINGW64 /d/工作学习/Go
$ ./helloword.exe 
hello world

程序结构

Go 源文件以 package 声明开头,说明源文件所属的包,接着使用 import 导入依赖的包,其
次为包级别的变量、常量、类型和函数的声明和赋值。函数中可定义局部的变量、常量等

基本组成元素

标识符

标识符是编程时所使用的名字,用于给变量、常量、函数、类型、接口、包名等进行命名,以建立名称和使用之间的关系,Go语言标识符的命名规则:

  • 只能由非空字母、数字、下划线组成
  • 只能以字母或者下划线开头
  • 不能使用Go关键字
  • 避免使用Go语言预定标识符
  • 建议使用驼峰命名,不强制
  • 标识符区分大小写

Go语言提供一些预定的标识符来表示内置的常量、类型、函数,在定义标识符时应避免使用

  • 内置常量:true、false、nil、iota
  • 内置类型:bool、byte、rune、int、int8、int16、int32、int64、uint、uint8、unit16、unit32、unit64、uintptr、float32、float64、complex64、complex128、string、error
  • 内置函数:make、len、cap、new、append、copy、close、delete、complex、real、imag、panic、recover
  • 空白标识符:_

关键字

关键字用于特性的语法解构,Go语言定义25个关键字:

  • 声明:import、package
  • 实体声明和定义:chan、const、func、interface、map、struct、type、var
  • 流程控制:break、case、continue、default、defer、else、fallthrough、for、go、goto、if、range、return、select、switch

字面量

字面量是值的表示方法,常用与对变量/常量进行初始化、主要分为:

  • 表示基础数据类型值的字面量,例如:0, 1.1, true, 3 + 4i, ‘a’, “我爱中国”
  • 构造自定义的复合数据类型的类型字面量,例如:type Interval int
  • 用于表示符合数据类型值的复合字面量,用来构造 array、slice、map、struct 的值,例如:{1, 2, 3}

操作符

  • 算术运算符:+-*/%++--
  • 关系运算符:>>=<<===!=
  • 逻辑运算符:&&||!
  • 位运算符:&|^<<>>&^
  • 赋值运算符:=+=-=*=/=%=&=|=^=<<=>>=
  • 其他运算符:&(单目)、*(单目)、.(点)、-(单目)、<-

分隔符

小括号(), 中括号[],大括号{},分号;,逗号,

变量

变量是指对一块存储空间定义名称,通过名称对存储空间的内容进行访问或修改,使用 var
进行变量声明,常用的语法为:

  1. var 变量名 变量类型 = 值
    定义变量并进行初始化
var name string = "jerry"
  1. var 变量名 变量类型
    定义变量使用零值进行初始化
var age int
  1. var 变量名 = 值
    定义变量,变量类型通过值类型进行推导
var isMale = true
  1. var 变量名 1, 变量名 2 , …, 变量名 n 变量类型
    定义多个相同类型的变量并使用零值进行初始化
var prefix, suffix string
  1. var 变量名 1, 变量名 2 , …, 变量名 n 变量类型 = 值 1, 值 2, …, 值 n
    定义多个相同类型的变量并使用对应的值进行初始化
var prev, next int = 3, 4
  1. var 变量名 1, 变量名 2 , …, 变量名 n = 值 1, 值 2, …, 值 n
    定义多个变量并使用对应的值进行初始化,变量的类型使用值类型进行推导,类型可不
    相同
var name, age = "tom", 18
  1. 批量定义
    定义多个变量并进行初始化,批量复制中变量类型可省略
var (
    name string = "jerry"
    age int = 18
)

变量简短声明

在函数内可以通过简短声明语句声明并初始化变量,可通过简短声明同时声明和初始化多个
变量,需要注意操作符左侧的变量至少有一个未定义过

gender := "male"

注意,简短声明只能在函数中使用,且变量短声明不能指定类型,类型都是通过推导出来

常量

常量用于定义不可被修改的的值,需要在编译过程中进行计算,只能为基础的数据类型布尔、
数值、字符串,使用 const 进行常量声明,常用语法:

  1. const 常量名 类型 = 值
    定义常量并进行初始化
const pi float64 = 3.1415926
  1. const 常量名 = 值
    定义常量,类型通过值类型进行推导
const e = 2.7182818
  1. 批量定义
    定义多个变量并进行初始化,批量赋值中变量类型可省略,并且除了第一个常量值外其他常量可同时省略类型和值,表示使用前一个常量的初始化表达式
const (
    name string = "baihuzi"
    desc
)

常量之间的运算,类型转换,以及对常量调用函数 len、cap、real、imag、complex、
unsafe.Sizeof 得到的结果依然为常量

作用域

作用域指变量可以使用范围。go 语言使用大括号显示的标识作用域范围,大括号内包含一连串的语句,叫做语句块。语句块可以嵌套,语句块内定义的变量不能在语句块外使用。作用域内定义变量只能被声明一次且变量必须使用,否则编译错误。在不同作用域可定义相同的变量,此时局部将覆盖全局
常见隐式语句块:

  • 全语句块
  • 包语句块
  • 文件语句块
  • if、switch、for、select、case 语句块

基础数据类型

布尔类型

布尔类型用于表示真假,类型名为 bool,只有两个值 true 和 false,占用一个字节宽度,零值为 false,常用操作:
逻辑运算
与(&&),只有左、右表达式结果都为 true,运算结果为 true

func main() {
    isTrue, isFalse := true, false
    fmt.Println(isTrue && isFalse)  // false
    fmt.Println(isTrue && isTrue)   // true
    fmt.Println(isFalse && isTrue)  // false
    fmt.Println(isFalse && isFalse) // false
}

或(||),只要左、右表达式有一个为 true,运算结果为 true

func main() {
    isTrue, isFalse := true, false
    fmt.Println(isTrue || isFalse)  // true
    fmt.Println(isTrue || isTrue)   // true
    fmt.Println(isFalse || isTrue)  // true
    fmt.Println(isFalse || isFalse) // false
}

非(!),右表达式为 true,运算结果为 false;右表达式为 false,运算结果为 true

func main() {
    isTrue, isFalse := true, false
    fmt.Println(!isTrue)  // false
    fmt.Println(!isFalse) // true
}

关系运算
等于(==),不等于(!=)

func main() { // 定义main函数
    isTrue, isFalse := true, false
    fmt.Println(isTrue == isFalse) // false
    fmt.Println(isTrue != isFalse) // true
}

数值类型

整型

Go 语言提供了5种有符号、5种无符号、1种指针、1种单字节、1种单个unicode字符(unicode码点),共13种整数类型,零值均为0

类型名 字节宽度 说明&取值范围
int 与平台有关,32位系统4字节,64位系统8字节 有符号整型
uint 与平台有关,32位系统4字节,64位系统8字节 无符号整形
rune 4 字节 Unicode 码点,取值范围同 uint32
int8 1 字节 用 8 位表示的有符号整型,取值范围为:[-128, 127]
int16 2 字节 用 16 位表示的有符号整型,取值范围为:[-32768, 32767]
int32 4 字节 用 32 位表示的有符号整型,取值范围为:[-2147483648, 2147483647]
int64 8 字节 用 64 位表示的有符号整型,取值范围为:[-9223372036854775808, 9223372036854775807]
uint8 1 字节 用 8 位表示的无符号整型,取值范围为:[0, 255]
uint16 2 字节 用 16 位表示的无符号整型,取值范围为:[0, 65535]
uint32 4 字节 用 32 位表示的无符号整型,取值范围为:[0, 4294967295]
uint64 8 字节 用 64 位表示的无符号整型,取值范围为:[0, 18446744073709551615]
byte 1 字节 字节类型,取值范围同 uint8
uintptr 与平台有关,32 位系统 4 字节,64 位系统 8 字节 指针值的无符号整型

进制

  • 十进制
    以 10 为基数,采用 0-9 十个数字,逢 10 进位,例如:10
  • 八进制
    以 8 为基数,采用 0-7 八个数字,逢 8 进位,使用 0 开头表示为八进制表示,例如:010
  • 十六进制
    以 16 为基数,采用 0-9 十个数字和 A-F 六个字母,逢 16 进位,使用0X 开头表示为十六进制 ,例如:0X10
func main() {
      fmt.Println(10, 010, 0x10) // 10 8 16
}

常用操作

算数运算, +、-、*、/、%、++、–

注意:针对/,除数不能为 0,且结果依然为整数

func main() {
    n1, n2 := 1, 2
    fmt.Println(n1 + n2) // 3
    fmt.Println(n1 - n2) // -1
    fmt.Println(n1 * n2) // 2
    fmt.Println(n1 / n2) // 0
    fmt.Println(n1 % n2) // 1

    n1++
    n2--
    fmt.Println(n1, n2) // 2 1
}

关系运算:>、>=、<、<=、==、!=

func main() {
    n1, n2 := 1, 2
    fmt.Println(n1 > n2)  // false
    fmt.Println(n1 < n2)  // true
    fmt.Println(n1 >= n2) // false
    fmt.Println(n1 <= n2) // true
    fmt.Println(n1 == n2) // false
    fmt.Println(n1 != n2) //true
}

位运算:&、|、^、&^、<<、>>
对于负整数在计算机中使用补码进行表示,对应正整数二进制表示取反+1,针对左、右移的右操作数必须为无符号整型

  • &:按位与,如果两个相应的二进制位都为1,则该位的结果值为1,否则为0
  • |:按位或,两个相应的二进制位中只要有一个为1,该位的结果值为1
  • ^:按位异或,若参加运算的两个二进制位值相同则为0,否则为1
  • &^:如果运算符右侧数值的第 i 位为 1,那么计算结果中的第 i 位为 0;如果运算符右侧数值的第 i 位为 0,那么计算结果中的第 i 位为运算符左侧数值的第 i 位的值。
  • <<:左移,各二进位全部左移若干位,正数高位补0,负数高位补1,低位丢弃
  • >>:右移,各二进位全部右移若干位,正数高位补0,负数高位补1,低位丢弃
func main() {
    n1, n2 := 1, 2
    fmt.Println(n1 & n2)
    // n1 = 1   0000 0000 0000 0000 0000 0000 0000 0001
    // n2 = 2   0000 0000 0000 0000 0000 0000 0000 0010
    // n1 & n2  0000 0000 0000 0000 0000 0000 0000 0000
    // 结果:0

    fmt.Println(n1 | n2)
    // n1 = 1   0000 0000 0000 0000 0000 0000 0000 0001
    // n2 = 2   0000 0000 0000 0000 0000 0000 0000 0010
    // n1 | n2  0000 0000 0000 0000 0000 0000 0000 0011
    // 结果:3

    fmt.Println(n1 ^ n2)
    // n1 = 1   0000 0000 0000 0000 0000 0000 0000 0001
    // n2 = 2   0000 0000 0000 0000 0000 0000 0000 0010
    // n1 ^ n2  0000 0000 0000 0000 0000 0000 0000 0011
    // 结果:3

    fmt.Println(n1 &^ n2)
    // n1 = 1   0000 0000 0000 0000 0000 0000 0000 0001
    // n2 = 2   0000 0000 0000 0000 0000 0000 0000 0010
    // n1 ^ n2  0000 0000 0000 0000 0000 0000 0000 0001
    // 结果:1

    fmt.Println(n1 << 2)
    // n1 = 1   0000 0000 0000 0000 0000 0000 0000 0001
    // n1 << 2  0000 0000 0000 0000 0000 0000 0000 0100
    // 结果:4

    fmt.Println(n1 >> 2)
    // n1 = 1   0000 0000 0000 0000 0000 0000 0000 0001
    // n1 >> 2  0000 0000 0000 0000 0000 0000 0000 0000
    // 结果:0
}

浮点型

浮点数用于表示带小数的数字,Go 提供 float32 和 float64 两种浮点类型
字面量:

  • 十进制表示法:3.1415926
  • 科学记数法:1e-5

复数型

Go 提供 complex64 和 complex128 两种复数类型,针对 complex64 复数的实部和虚部均使用float32,针对 complex128 复数的实部和虚部均使用 float64

字面量:

  • 十进制表示法:1 + 2ii*i = -1, 1 为实部,2 为虚部

常用函数:

  • complex: 工厂函数,通过两个参数创建一个复数
  • real:用于获取复数的实部
  • imag: 用于获取复数的虚部
func main() {
    var (
        c1 complex64 = 1 + 2i
        c2 complex64 = complex(3, 4)
    )

    fmt.Println(c1, c2)             // (1+2i) (3+4i)
    fmt.Println(real(c2), imag(c2)) // 3 4
}

字符串

Go 语言内置了字符串类型,使用 string 表示
字面量:

  • 可解析字符串:通过双引号(“)来创建,不能包含多行,支持特殊字符转义序列
  • 原生字符串:通过反引号(`)来创建,可包含多行,不支持特殊字符转义序列

特殊字符:

  • \:反斜线
  • ':单引号
  • ":双引号
  • \a:响铃
  • \b:退格
  • \f:换页
  • \n:换行
  • \r:回车
  • \t:制表符
  • \v:垂直制表符
  • \ooo:3 个 8 位数字给定的八进制码点的 Unicode 字符(不能超过\377) - \uhhhh:4 个 16 位数字给定的十六进制码点的 Unicode 字符
  • \Uhhhhhhhh:8 个 32 位数字给定的十六进制码点的 Unicode 字符
  • \xhh:2 个 8 位数字给定的十六进制码点的 Unicode 字符

常用操作

  • 字符串连接:+
  • 关系运算符:>、>=、<、<=、==、!=
  • 赋值运算符:+=
  • 索引:s[index],针对只包含 ascii 字符的字符串
  • 切片:s[start:end] ,针对只包含 ascii 字符的字符串

字符串遍历,遍历的结果是码点,且由于中文和英文占的字节数不同,遍历的索引位也不相同,GBK编码,一个汉字占两个字节。 UTF-8编码是变长编码,通常汉字占三个字节,因此遍历的结果如下所示

func main() {

    str := "我不是药神abcde"
    for i, v := range str {
      fmt.Println(i, v)
    }

    // 0 25105
    // 3 19981
    // 6 26159
    // 9 33647
    // 12 31070
    // 15 97
    // 16 98
    // 17 99
    // 18 100
    // 19 101
}

枚举类型

go 中并没有枚举类型,但是可以通过 const 来模拟枚举,常使用 iota 生成器用于初始化一系列相同规则的常量,批量声明常量的第一个常量使用iota 进行赋值,此时 iota 被重置为 0,其他常量省略类型和赋值,在每初始化一个常量则加 1

func main() {
    const (
        SUNDAY    int = 0
        MONDAY    int = 1
        TUESDAY   int = 2
        WEDNESDAY int = 3
        THURSDAY  int = 4
        FRIDAY    int = 5
        SATURDAY  int = 6
    )

    fmt.Println(SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY)
    // 0 1 2 3 4 5 6
}
func main() {
    // 在文件中使用的 const 来模拟枚举,每次都要定义一个数,非常累赘;
    // 这里可以引入一个 iota 变量,这个变量有个特点就是在【同一个小括号内】,每调用一次,就会+1,类似一个每次+1的生成器;
    const (
        SUNDAY = iota
        MONDAY
        TUESDAY
        WEDNESDAY
        THURSDAY
        FRIDAY
        SATURDAY
    )
    fmt.Println(SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY)
    // 0 1 2 3 4 5 6
}

注意iota的特性

func main() {
    const (
        n1 = iota
        n2
        n3
        n4 = 100
        n5 = iota
        n6
    )

    fmt.Println(n1, n2, n3, n4, n5, n6)
    // 0 1 2 100 4 5
}

指针类型

每个变量在内存中都有对应存储位置(内存地址),可以通过&运算符获取。指针是用来存储变量地址的变量

  • 声明:指针声明需要指定存储地址中对应数据的类型,并使用*作为类型前缀。指针变量声明后会被初始化为 nil,表示空指针
  • 初始化:
    1. 使用&运算符+变量初始化:&运算获取变量的存储位置来初始化指针变量
    2. 使用 new 函数初始化:new 函数根据数据类型申请内存空间并使用零值填充,并返回申请空间地址
  • 操作:可通过*运算符+指针变量名来访问和修改对应存储位置的值
func main() {
    // 对于数字这种数据类型,当使用一个变量接收另一个变量时,会在内存中开辟一块新的内存地址用来保存,从而无法修改原始变量

    var num int = 100
    var num1 int = num
    num1 = 200
    fmt.Println(num, num1) // 100 200

    num3 := 500
    var num4 *int = &num3 // *int 表示指针类型  同理  *string 也是,在类型前加上*即表示指针  &可以拿到一个变量的指针
    *num4 = 600
    fmt.Println(num3, *num4) // 600 600

    // 可以申请一个空指针,用来存储数据
    num5 := new(int)         // num5此时是一个指针  *int类型,默认是零值 0
    fmt.Println(num5, *num5) // *int 0
    *num5 = 999
    fmt.Println(num5, *num5) // *int 999

    str1 := new(string)
    fmt.Println(str1, *str1) // *string   ""
}

流程控制

Go语言中的流程控制和其他编程语言基本一样,可能就是语法上的差异而已,流程控制的逻辑都是一样的,需要说明的是,在Go语言中,是没有while循环的,需要使用for循环代替。还有switch case语法,和js是不一样的,这些都是根据语言语法规定的。

func main() {
    // if else
    condition := true
    if condition == true {
        fmt.Println("true")
    } else {
          fmt.Println("false")
    }

    // if else if
    score := 100
    if score == 100 {
          fmt.Println("pretty good")
    } else if score >= 90 {
          fmt.Println("good")
    } else if score >= 60 {
          fmt.Println("ok")
    } else {
          fmt.Println("bad")
    }

    // switch case
    switch {
        case score == 100:
        fmt.Println("pretty good")
        case score >= 90:
        fmt.Println("good")
        case score >= 60:
        fmt.Println("ok")
        default:
        fmt.Println("bad")
    }

    // for循环
    sum := 0
    for index := 1; index <= 100; index++ {
          sum += index
    }
    fmt.Println(sum)

    // go 中没有while循环,使用for循环代替
    sum1 := 0
    index1 := 1
    for index1 <= 100 {
        sum1 += index1
        index1++
    }
    fmt.Println(sum1)

    // 死循环
    sum2 := 0
    index2 := 1
    for {
        if index2 > 100 {
              break
        }
        sum2 += index2
        index2++
    }
    fmt.Println(sum2)
}

跳出循环

  • break:跳出当前循环
  • continue:跳出本次循环,开始下一次循环
  • break [LABEL]:结合label跳出外层循环
func main() {
    for i := 0; i <= 10; i++ {
        if i == 5 {
               break // break 跳出循环
        }
        fmt.Println(i)
    }
    fmt.Println("-----------------------")
    for j := 0; j <= 10; j++ {
        if j == 5 {
            continue // continue 跳出本次循环,开始下一次循环
        }
        fmt.Println(j)
    }
    fmt.Println("-----------------------")

    // 默认 break 只能跳出当前循环,不能跳出外级循环
    for k := 0; k <= 10; k++ {
        for u := 0; u <= 10; u++ {
            if u == 5 {
                  break // continue 跳出本次循环,开始下一次循环
            }
            fmt.Println(k, u)
        }
    }

    fmt.Println("-----------------------")
    // 如果想要跳出外层循环,需要结合label
    LOOP:
    for x := 0; x <= 10; x++ {
        for y := 0; y <= 10; y++ {
            if y == 5 {
                  break LOOP // continue 跳出本次循环,开始下一次循环
            }
            fmt.Println(x, y)
        }
    }
    // 和js里面的一样
}

label和goto

goto 表示代码运行过程中跳转到哪里,START 和 END 不是固定的,可以自己取值

func main() {
    sum := 0
    index := 1
    START:
    if index > 100 {
          goto END
    }
    sum += index
    index++
    goto START
    END:
    fmt.Println(sum)
}

复合数据类型

数组

数组声明需要指定组成元素的类型以及存储元素的数量(长度)。在数组声明后,其长度不可修改,数组的每个元素会根据对应类型的零值对进行初始化。

字面量声明数组

  • 指定数组长度: [length]type{v1, v2, …, vlength}
  • 使用初始化元素数量推导数组长度: […]type{v1, v2, …, vlength}
  • 对指定位置元素进行初始化: [length]type{im:vm, …, sin:in}
func main() {

    // go 中数组不可删除和添加  因为长度会影响类型,类型固定后就不能添加和删除了,和js不同
    // 长度一旦固定则不可变,一般在go中很少使用,一般使用切片
    var arr1 [10]int = [10]int{}
    fmt.Printf("%T, %v \n", arr1, arr1) // [10]int, [0 0 0 0 0 0 0 0 0 0]

    arr2 := [10]int{}
    fmt.Printf("%T, %v \n", arr2, arr2) // [10]int, [0 0 0 0 0 0 0 0 0 0]

    arr3 := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Printf("%T, %v \n", arr3, arr3) // [10]int, [1 2 3 4 5 6 7 8 9 10]

    arr4 := [10]int{1, 2, 3, 4, 5}
    fmt.Printf("%T, %v \n", arr4, arr4) // [10]int, [1 2 3 4 5 0 0 0 0 0]

    arr5 := [10]int{0: 1, 3: 2, 5: 3}
    fmt.Printf("%T, %v \n", arr5, arr5) // [10]int, [1 0 0 2 0 3 0 0 0 0]

    str1 := [5]string{}
    fmt.Printf("%T, %q \n", str1, str1) // [5]string, ["" "" "" "" ""]

    str2 := [5]string{"abc", "def"}
    fmt.Printf("%T, %q \n", str2, str2) // [5]string, ["abc" "def" "" "" ""]

    str3 := [5]string{0: "abc", 3: "def"}
    fmt.Printf("%T, %q \n", str3, str3) // [5]string, ["abc" "" "" "def" ""]

    // ...推导
    var arr6 [10]int
    fmt.Printf("%T, %v \n", arr6, arr6) // [10]int, [0 0 0 0 0 0 0 0 0 0]

    arr6 = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} // ... 会按照后面给定的个数自动推导
    fmt.Printf("%T, %v \n", arr6, arr6)            // [10]int, [1 2 3 4 5 6 7 8 9 10]

    // arr6 = [...]int{1,2,3}		// 报错 cannot use [...]int{…} (value of type [3]int) as type [10]int in assignment

    for i := 0; i < len(arr6); i++ {
          fmt.Println(arr6[i])
    }

    for index := range arr6 {
          fmt.Println(arr6[index])
    }

    for index, value := range arr6 {
          fmt.Println(index, value)
    }

    for _, value := range arr6 {
        fmt.Println(value)
    }
  }

切片

切片是长度可变的数组(具有相同数据类型的数据项组成的一组长度可变的序列),切片由三部分组成:

  • 指针:指向切片第一个元素指向的数组元素的地址
  • 长度:切片元素的数量
  • 容量:切片开始到结束位置元素的数量
func main() {
    // 切片是相同类型长度可变的元素集合

    // nil切片, 不赋值,可以理解威切片的零值是nil
    var slice []int
    fmt.Println(slice, slice == nil) // [] true

    // 空切片
    slice1 := []int{}
    fmt.Println(slice1, slice1 == nil) // [] false

    slice2 := []int{0: 1, 10: 10}
    fmt.Println(slice2) // [1 0 0 0 0 0 0 0 0 0 10]

    slice2[4] = 4
    fmt.Println(slice2) // [1 0 0 0 4 0 0 0 0 0 10]

    // 通过make函数来创建切片,类似于js中的 new Array(5).fill(0)
    slice3 := make([]int, 5)
    fmt.Println(slice3) // [0 0 0 0 0]

    slice4 := make([]string, 5)
    fmt.Printf("%q \n", slice4) // ["" "" "" "" ""]

    // 切片容量
    // 通过make函数的第三个参数,可以指定切片的容量
    // 切片在底层使用数组进行数据存储,数组是有固定长度的,当切片的长度超过了数组的长度,切片将会申请一个更长的数组,再将原来的数组拷贝过来
    slice5 := make([]int, 5, 10)
    fmt.Println(slice5, len(slice5), cap(slice5)) // [0 0 0 0 0] 5 10

    // 切片可以通过数组进行切片操作来获得,切片的长度既截取出来的长度3,切片的容量既数组的长度减去开始索引位 10 - 2 = 8
    arr1 := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    slice6 := arr1[2:5]
    fmt.Println(slice6, len(slice6), cap(slice6)) // [3 4 5] 3 8

    // !!!需要注意的是,通过数组切片操作获得的切片,底层共享数组的数据
    // 可以看到我们此时将数组的第五个元素修改成为100,切片slice6同样会发生修改,使用时需要特殊注意
    arr1[4] = 100
    fmt.Println(arr1, slice6) // [1 2 3 4 100 6 7 8 9 10] [3 4 100]

    // 遍历,和遍历数组一样,可以通过下标或者range
    slice7 := []int{1, 2, 3, 4, 5}
    for i := 0; i < len(slice7); i++ {
        fmt.Println(slice7[i])
    }

    for index, value := range slice7 {
        fmt.Println(index, value)
    }

    // 添加 append(slice, item1, item2, item3, ...),append接收一个添加了切片的返回值,不会直接在原始切片中添加元素
    slice8 := append(slice7, 6, 7, 8)
    fmt.Println(slice7, slice8) // [1 2 3 4 5] [1 2 3 4 5 6 7 8]

    // 删除,在go中没有直接提供删除切片元素的方法,例如js中的pop shift splice
    // 可以使用切片操作和解构来进行切片的删除
    slice9 := []int{1, 2, 3, 4, 5}

    // 删除最后一个元素
    slice10 := slice9[0 : len(slice9)-1]
    fmt.Println(slice9, slice10) // [1 2 3 4 5] [1 2 3 4]

    // 删除第一个元素
    slice11 := slice9[1:len(slice9)] // 如果取到末尾,len(slice9)可省略
    fmt.Println(slice9, slice11)     // [1 2 3 4 5] [2 3 4 5]

    // 删除中间元素, 比如删除3,索引位2的元素
    slice12 := append(slice9[0:2], slice9[3:]...) // 这里注意,会修改原始slice9切片, go中的解包操作符...是写在后面,而js中是写在前面
    fmt.Println(slice9, slice12)                  // [1 2 4 5 5] [1 2 4 5]

    // 切片的切片
    slice13 := make([]int, 5, 10)
    slice13[0] = 1
    slice13[1] = 2
    slice13[2] = 3
    fmt.Println(slice13, len(slice13), cap(slice13)) // [1 2 3 0 0] 5 10

    // 切片的endIndex可以超过长度,但不能超过容量,没有赋值的部分取零值
    fmt.Println(slice13[1:10]) // [2 3 0 0 0 0 0 0 0]

    // copy函数
    // 如果src length 和 dst length 相等,则直接进行覆盖
    dst := make([]int, 3)
    src := []int{1, 2, 3}
    copy(dst, src)
    fmt.Println(dst, src) // [1 2 3] [1 2 3]

    // 如果src length > dst length,则多出的部分会被舍弃
    dst1 := make([]int, 3)
    src1 := []int{1, 2, 3, 4}
    copy(dst1, src1)
    fmt.Println(dst, src) // [1 2 3] [1 2 3]

    // 如果src length < dst length,则只会覆盖src存在的部分
    dst2 := make([]int, 3)
    src2 := []int{1, 2}
    copy(dst2, src2)
    fmt.Println(dst2, src2) // [1 2 0] [1 2]
}

映射

映射是存储一系列无序的 key/value 对,通过 key 来对 value 进行操作(增、删、改、查)。映射的 key 只能为可使用==运算符的值类型(字符串、数字、布尔、数组),value 可以为任意类型 s。

map 声明需要指定组成元素 key 和 value 的类型,在声明后,会被初始化为 nil,表示暂不存在的映射

初始化

  • 使用字面量初始化:map[ktype]vtype{k1:v1, k2:v2, …, kn:vn}
  • 使用字面量初始化空映射:map[ktype]vtype{ }
  • 使用 make 函数初始化
    make(map[ktype]vtype),通过 make 函数创建映射
func main() {
    // key value 的无序集合, 遍历时不保证顺序,在go中通过hash table实现
    // 定义  map[keyType]valueType,零值是nil

    // 字面量定义,零值是nil
    var map1 map[string]string
    fmt.Printf("%T %v \n", map1, map1 == nil) // map[string]string true

    // 字面量定义并赋值
    map2 := map[string]int{"key1": 1}
    fmt.Printf("%v %v \n", map2, map2 == nil) // map[key1:1] false

    // 增
    // 直接对key进行赋值,map是引用类型
    mapNew := map2
    map2["newKey"] = 11
    fmt.Println(map2, mapNew) // map[key1:1 newKey:11] map[key1:1 newKey:11]

    // 删
    // 调用内置函数delete
    delete(map2, "newKey")
    fmt.Println(map2) // map[key1:1]

    // 改
    // 直接对key的value进行修改
    map2["key1"] = 999
    fmt.Println(map2) // map[key1:999]

    // 查
    // 如果key对应的value存在,则直接返回对应的value
    value := map2["key1"]
    fmt.Println(value) // 999

    // 如果key对应的value不存在,则返回value类型的零值
    value2 := map2["key2"]
    fmt.Println(value2) // 0

    // 判断对应的value存不存在,可以接受第二个返回值,布尔类型,即对应key的value存不存在,不需要自己取值手动判断
    value3, exist3 := map2["key1"]
    fmt.Println(value3, exist3) // 1 true
    value4, exist4 := map2["key2"]
    fmt.Println(value4, exist4) // 0 false

    // 遍历,不保证顺序,没有index,只能用range遍历

    for key := range map2 {
        fmt.Println(key, map2[key])
    } // key1 999

    for key, value := range map2 {
        fmt.Println(key, value)
    } // key1 999

    // 通过make创建map
    map3 := make(map[int]string)
    map3[0] = "0"
    map3[1] = "1"
    fmt.Println(map3) // map[0:0 1:1]

}

函数

函数用于对代码块的逻辑封装,提供代码复用的最基本方式,函数也可以赋值给变量,存储在数组、切片、映射中,也可作为参数传递给函数或作为函数返回值进行返回。函数的入参是值传递,传递进来的参数会进行一个拷贝,如果在函数内部需要修改传进来的入参,需要传递指针,用来修改传进来的原始入参值

package main

import "fmt"

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

// 任意多个参数
func anyArgs(args ...string) {
    fmt.Printf("%T %v \n", args, args) // []string [1 2 3]
}

func sumFunc(n ...int) int {
    sum := 0
    for _, v := range n {
        sum += v
    }
    return sum
}

// 多个返回值
func calc(a, b int) (int, int, int, int) {
    return a + b, a - b, a * b, a / b
}

// 命名返回值,返回类型中定义的参数,将直接再函数作用域中申明,并使用零值初始化,在函数体内部不需要重复定义
// 并可以直接return,无需加上返回参数
func sumFunc2(n ...int) (sum int) {
    for _, v := range n {
        sum += v
    }
    return
}

func calc2(a, b int) (jia, jian, cheng, chu int) {
    jia = a + b
    jian = a - b
    cheng = a * b
    chu = a / b
    return
}

// 闭包,闭包就是在一个函数中返回另一个函数,在返回函数的内部使用到了外层函数的变量,这个变量在函数执行完的时候并不会被销毁,留着别删
func bb() func() string {
    name := "123"
    return func() string {
        return name
    }
}

func main() {
    sum := add(1, 2)
    fmt.Println(sum) // 3

    anyArgs("1", "2", "3")

    fmt.Println(sumFunc(1, 2, 3, 4, 5)) // 15

    r1, r2, r3, r4 := calc(4, 2)
    fmt.Printf("%v %v %v %v \n", r1, r2, r3, r4) // 6 2 8 2

    fmt.Println(sumFunc2(1, 2, 3, 4)) // 10

    rt1, rt2, rt3, rt4 := calc2(6, 2)
    fmt.Printf("%v %v %v %v \n", rt1, rt2, rt3, rt4) // 8 4 12 3
}

错误处理

Go 语言通过 error 接口实现错误处理的标准模式,通过使用函数返回值列表中的最后一个值返回错误信息,将错误的处理交由程序员主动进行处理

package main

import (
    "errors"
    "fmt"
)

// go标准建议显式的将错误信息返回给开发者,由调用者自行处理
// 返回错误一般有两种方法,如fact函数中注释处的用法

func fact(n int64) (int64, error) {
    if n < 0 {
        return 0, fmt.Errorf("计算错误") // 和下面的方式一样
    }

    if n == 0 || n == 1 {
        return 1, nil
    }

    r, err := fact(n - 1)
    if err == nil {
        return n * r, nil
    }
    return 0, errors.New("计算错误") // 和上面的方式一样
}

// panic 抛出错误,通过panic抛出的错误,程序将直接终止,如果错误是一个可恢复的,我们需要保持程序运行,还是需要对错误进行手动处理
// 此时我们还是需要将其转换成为error的方式,通过defer函数调用和recover恢复函数执行,可以将其转换,是一种常见的方式

func fact1(n int64) int64 {
    if n < 0 {
        panic("n不能小于0")
    }

    if n == 0 || n == 1 {
        return 1
    }

    return n * fact1(n-1)
}

func main() {

    r, err := fact(5)
    fmt.Printf("%v %v\n", r, err) // 120 nil

    r1, err1 := fact(-1)
    fmt.Printf("%v %v\n", r1, err1) // 0 计算错误

    fact1(-1) // 直接调用,此时程序会直接终止
}

panic 与 recover 函数

go 语言提供 panic 和 recover 函数用于处理运行时错误,当调用 panic 抛出错误,中断原有的控制流程,常用于不可修复性错误。recover 函数用于终止错误处理流程,仅在 defer 语句的函数中有效,用于截取错误处理流程,recover 只能捕获到最后一个错误

package main

import (
    "fmt"
)

// panic 抛出错误,通过panic抛出的错误,程序将直接终止,如果错误是一个可恢复的,我们需要保持程序运行,还是需要对错误进行手动处理
// 此时我们还是需要将其转换成为error的方式,通过defer函数调用和recover恢复函数执行,可以将其转换,是一种常见的方式
// defer执行时机是其所在函数退出的时候执行,并且是一个堆栈解构,defer中的函数会存起来,当所在函数结束后,再取出来执行,由于是堆栈解构,先进后出,会优先执行代码上下文中靠后的defer调用

func fact(n int64) int64 {
    if n < 0 {
        panic("n不能小于0")
    }

    if n == 0 || n == 1 {
        return 1
    }

    return n * fact(n-1)
}

func warpedErrorFact(n int64) (r int64, err error) {
    defer func() {
        errMsg := recover()
        if errMsg != nil {
            err = fmt.Errorf("%v", errMsg)
        }
    }()
    r = fact(n)
    return
}

func main() {

    r, err := warpedErrorFact(-5)
    fmt.Println(r, err) // 0 n不能小于0

    r1, err1 := warpedErrorFact(5)
    fmt.Println(r1, err1) // 120 <nil>
}

defer

defer 关键字用户声明函数,不论函数是否发生错误都在函数执行最后执行(return 之前),若使用 defer 声明多个函数,则按照声明的顺序,先声明后执行(堆)常用来做资源释放,记录日志等工作

package main

import "fmt"

func main() {

    defer func() {
        fmt.Println("函数延迟执行1")
    }()
    defer func() {
        fmt.Println("函数延迟执行2")
    }()
    defer func() {
        fmt.Println("函数延迟执行3")
    }()

    fmt.Println("main")
}
main
函数延迟执行3
函数延迟执行2
函数延迟执行1

包是函数和数据的集合,将有相关特性的函数和数据放在统一的文件/目录进行管理,每个包都可以作为独立的单元维护并提供给其他项目进行使用

Go 源文件都需要在开头使用 package 声明所在包,包名告知编译器哪些是包的源代码用于编译库文件,其次包名用于限制包内成员对外的可见性,最后包名用于在包外对公开成员的访问

声明

包名使用简短的小写字母,常与所在目录名保持一致,一个包中可以由多个 Go 源文件,但必须使用相同包名

导入&调用

在使用包时需要使用 import 将包按路径名导入,再通过包名调用成员,可通过 import 每行导入一个包,也可使用括号包含所有包并使用一个 import 导入

编译&运行

使用 go build 编译二进制文件
命令:go build gpkgmain
说明:编译路径 gpkgmain 下的包,main 包,则在当前目录产生以 pkgmain 命名的
二进制程序

  • 使用 go run 运行二进制文件
    命令:go run gpkgmain
  • 使用 go install 编译并发布二进制文件
    命令:go install gpkgmain
    说明:编译并发布路径 gpkgmain 下的包,main 包,则在将编译后的以 pkgmain 命
    名的二进制程序拷贝到 bin 目录
  • 使用 go install 编译发布库文件
    命令:go install gpkgname/pkg01
    说明:编译并发布路径 gpkgname/pkg01 下的包,非 main 包,则在将编译的以包名
    命名的库文件拷贝到 pkg/GOOS_GOARCH 目录下 - 使用 go install 编译发布所有二进制和库文件
    命令:go install ./…

导入形式

  1. 绝对路径导入
    在 GOPATH 目录中查找包
    示例:
    import “fmt”
    import “gpkgname/pkg01”
  2. 相对路径导入
    在当前文件所在的目录查找
    示例:
    import “./gpkgname/pkg02”
  3. 点导入
    在调用点导入包中的成员时可以直接使用成员名称进行调用(省略包名)
package main

import . "fmt"

func main () {
      Println("Hello world")
}
  1. 别名导入
    当导入不同路径的相同包名时,可以别名导入为包重命名,避免冲突
package main

import f "fmt"

func main () {
    f.Println("Hello world")
}
  1. 下划线导入
    Go 不允许包导入但未使用,在某些情况下需要初始化包,使用空白符作为别名进行导入,从而使得包中的初始化函数可以执行

成员可见性

Go 语言使用名称首字母大小写来判断对象(常量、变量、函数、类型、结构体、方法等)的访问权限,首字母大写标识包外可见(公开的),否者仅包内可访问(内部的)

main 包与 main 函数

main 包用于声明告知编译器将包编译为二进制可执行文件,在 main 包中的 main 函数是程序的入口,无返回值,无参数

init 函数

init 函数是初始化包使用,无返回值,无参数。建议每个包只定义一个。init 函数在 import包时自动被调用(const->var->init)

包管理

介绍

Go1.11 版本提供 Go modules 机制对包进行管理,同时保留 GOPATH 和 vendor 机制,使用临时环境变量 GO111MODULE 进行控制,GO111MODULE 有三个可选值:

  • 当 GO111MODULE 为 off 时,构建项目始终在 GOPATH 和 vendor 目录搜索目标程序依赖包
  • 当 GO111MODULE 为 on 时,构建项目则始终使用 Go modules 机制,在 GOPATH/pkg/mod目录搜索目标程序依赖包
  • 当 GO111MODULE 为 auto(默认)时,当构建源代码不在 GOPATH/src 的子目录且包含go.mod 文件,则使用 Go modules 机制,否则使用 GOPATH 和 vendor 机制

GOPATH+vendor 机制

  • vendor
    将项目依赖包拷贝到项目下的 vendor 目录,在编译时使用项目下 vendor 目录中的包进行编译
    解决问题:
    外部包过多,在使用第三方包时需要使用 go get 进行下载
    第三方包在 go get 下载后不能保证开发和编译时版本的兼容性

  • 包搜索顺序
    1.在当前包下的 vendor 目录查找
    2.向上级目录查找,直到 GOPATH/src/vendor 目录
    3.在 GOPATH 目录查找
    4.在 GOROOT 目录查找

  • 第三方包
    可以借助 go get 工具下载和安装第三方包及其依赖,需要安装与第三方包匹配的代码管理工具,比如 git、svn 等

Go modules 机制

优势:

  • 不用设置 GOPATH,代码可任意放置
  • 自动下载依赖管理
  • 版本控制
  • 不允许使用相对导入
  • replace 机制

初始化模块
命令:go mod init modname

当前模块下的包
对于当前模块下的包导入时需要使用 modname+packagename

第三方包
在使用 go mod tidy、go build、go test、go list 命令会自动将第三方依赖包写入到go.mod 文件中同时下载第三方依赖包到 GOPATH/pkg/mod/cache 目录,并在当前模块目录生成一个构建状态跟踪文件 go.sum,文件中记录当前 module 所有的顶层和间接依赖,以及这些依赖的校验和

常用命令

  • go mod tidy:整理依赖模块(添加新增的,删除未使用的)
  • go mod vendor: 将依赖模块拷贝到模块中的 vendor 目录
  • go build: 编译当前模块
  • go build ./…: 编译当前目录下的所有模块
  • go build -mod=vendor:使用当前模块下的 vendor 目录中的包进行编译
  • go mod download: 仅下载第三方模块
  • go mod grapha: 打印所有第三方模块
  • go list -m -json all:显示所有模块信息
  • go mod edit: 修改 go.mod 文件
    require=pakcage@version
    replace=old_package@version=new_package@version
    可以使用-replace 功能将包替换为本地包,实现相对导入

结构体

自定义类型

自定义类型类似于别名,可以将go的类型自定义为自己的类型,这个类型的特性和原始go的特性一致,也可用于类型转换

type Counter int

定义结构体

结构体定义使用 struct 标识,需要指定其包含的属性(名和类型),在定义结构体时可以为结构体指定结构体名(命名结构体),用于后续声明结构体变量使用。

type User struct {
    id    string
    name  string
    age   int
    birth time.Time
    tel   string
    addr  string
}

声明结构体

声明结构体变量只需要定义变量类型为结构体名,变量中的每个属性被初始化为对应类型的零值。也可声明结构体指针变量,此时变量被初始化为 nil

package main

import (
    "fmt"
    "time"
)

type User struct {
    id    string
    name  string
    age   int
    birth time.Time
    tel   string
    addr  string
}

func main() {

    var u1 User
    fmt.Println(u1) // {  0 {0 0 <nil>}  }

    var u2 *User
    fmt.Println(u2) // <nil>
}

结构体赋值

使用结构体创建的变量叫做对应结构体的实例或者对象

  1. 使用结构体零值初始化结构体值对象
package main

import (
    "fmt"
    "time"
)

type User struct {
    id    string
    name  string
    age   int
    birth time.Time
    tel   string
    addr  string
}

func main() {
    var u1 User = User{}
    fmt.Println(u1) // {  0 {0 0 <nil>}  }
}
  1. 使用结构体字面量初始化结构体值对象
  • 使用结构体零值初始化结构体值对象
  • 使用结构体字面量初始化结构体值对象
  • 使用 new 函数进行初始化结构体指针对象
  • 使用结构体字面量初始化结构体指针对象
package main

import (
    "fmt"
)

type User struct {
    id   string
    name string
    age  int
    tel  string
    addr string
}

func main() {

    var u1 User = User{"001", "jerry", 18, "13299999999", "湖北"}
    var u2 User = User{id: "002"}
    var u3 *User = new(User)
    u3.id = "003"
    u4 := &User{id: "004"}
    fmt.Println(u1) // {001 jerry 18 13299999999 湖北}
    fmt.Println(u2) // {002  0  }
    fmt.Println(u3) // &{003  0  }
    fmt.Println(u4) // &{004  0  }
}

属性的访问和修改

通过结构体对象名/结构体指针对象.属性名的方式来访问和修改对象的属性值

package main

import "fmt"

type User struct {
    id   string
    name string
    age  int
    tel  string
    addr string
}

func main() {

    var u1 User = User{id: "001", name: "jerry"}
    fmt.Println(u1.id, u1.name) // 001 jerry

    u1.id = "1001"
    u1.name = "tom"
    fmt.Println(u1.id, u1.name) // 1001 tom
  
      var u2 *User = new(User)
    fmt.Println(u2)

    (*u2).id = "002"
    u2.name = "jack"
    fmt.Println(u2) // &{002 jack 0  }

    u2.id = "003"	// 这里不取指针也可以对属性进行赋值
    fmt.Println(u2) // &{003 jack 0  }
}

匿名结构体

在定义变量时将类型指定为结构体的结构,此时叫匿名结构体。匿名结构体常用于初始化一次结构体变量的场景,例如项目配置

package main

import "fmt"

func main() {
    var connect = struct {
        ip       string
        port     string
        user     string
        password string
    }{"127.0.0.1", "8080", "root", "root@password"}
    fmt.Println(connect)
}

命名嵌入

结构体命名嵌入是指结构体中的属性对应的类型也是结构体,使用.链式调用修改或者访问值

package main

import "fmt"

type Address struct {
    country  string
    province string
}

type User struct {
    id   string
    name string
    addr Address // 命名嵌入
}

func main() {
    var u1 User = User{id: "001", name: "jerry", addr: Address{country: "china", province: "hubei"}}
    fmt.Println(u1) // {001 jerry {china hubei}}

    var u2Addr = Address{"中国", "湖北"}
    var u2 User = User{"002", "tom", u2Addr}
    fmt.Println(u2) // {002 tom {中国 湖北}}
  
      u1.addr.province = "广东"
    fmt.Println(u1) // {001 jerry {china 广东}}
}

匿名嵌入

结构体匿名嵌入是指将已定义的结构体名直接声明在新的结构体中,从而实现对以后已有类型的扩展和修改

package main

import "fmt"

type Address struct {
    country  string
    province string
}

type User struct {
    id   string
    name string
    Address
}

func main() {
    var u1 User = User{"001", "jerry", Address{"china", "hubei"}}
    fmt.Println(u1) // {001 jerry {china hubei}}

    var u2 User = User{"002", "tom", Address{country: "china", province: "hubei"}}
    fmt.Println(u2) // {002 tom {china hubei}}

    var u3 User = User{id: "003", name: "jack", Address: Address{country: "china", province: "hubei"}}
    fmt.Println(u3) // {003 jack {china hubei}}

    fmt.Println(u1.country)  // china
    fmt.Println(u2.province) // hubei
    fmt.Println(u3.country)  // china

    // 上述访问或修改属性的方法同样可以使用链式调用,只是可以省略其中匿名嵌套的结构体名
    fmt.Println(u1.Address.country)  // china
    fmt.Println(u2.Address.province) // hubei
    fmt.Println(u3.Address.country)  // china
}

下面声明方式是不通过的,通过下面的情况可以得出,匿名结构体嵌套实际上不是扩展,而是使用和结构体相同的名称进行命名,只是省略了名字,类似于下面的方式。注意是类似,因为下面这样声明实际上是命名嵌套了。

type User struct {
    id      string
    name    string
    Address Address
}
// 不能使用下面方式声明
// var u4 User = User{"003", "jack", "china", "hubei"}
// var u5 User = User{id: "003", name: "jack", country: "china", province: "hubei"}

指针类型嵌入

结构体嵌入(命名&匿名)类型也可以为结构体指针,且由于指针的特性,当指针的值修改了之后,将会影响所有使用此指针的结构体

package main

import "fmt"

type Address struct {
    country  string
    province string
}

type User struct {
    id   string
    name string
    addr *Address
}

func main() {
    
    addr := &Address{"china", "hubei"}
    var u1 User = User{"001", "jerry", addr}
    var u2 User = User{"002", "tom", addr}
    
    addr.country = "中国"
    fmt.Printf("%#v\n", u1.addr.country) // "中国"
    fmt.Printf("%#v\n", u2.addr.country) // "中国"
}

可见性

结构体首字母大写则包外可见(公开的),否者仅包内可访问(内部的)
结构体属性名首字母大写包外可见(公开的),否者仅包内可访问(内部的)
组合:

  • 结构体名首字母大写,属性名大写:结构体可在包外使用,且访问其大写的属性名
  • 结构体名首字母大写,属性名小写:结构体可在包外使用,且不能访问其小写的属性名
  • 结构体名首字母小写,属性名大写:结构体只能在包内使用,属性访问在结构体嵌入时
    由被嵌入结构体(外层)决定,被嵌入结构体名首字母大写时属性名包外可见,否者只能在包内使用
  • 结构体名首字母小写,属性名小写:结构体只能在包内使用

方法

定义

方法是添加了接收者的函数,接收者必须是自定义的类型

调用

调用方法通过自定义类型的对象.方法名进行调用,在调用过程中对象传递(赋值)给方法的接收者(值类型,拷贝)

package main

import "fmt"

type Dog struct {
    name string
}

func (d Dog) Run() {
    fmt.Printf("%s running", d.name)
}

func (d Dog) RenameOne(name string) {
    d.name = name
}

func (d *Dog) RenameTwo(name string) {
    d.name = name
}

func main() {
    var d1 Dog = Dog{"dahuang"}
    d1.Run() // dahuang running

    // 由于传递给方法的变量时值传递,因此这里rename是不会修改传入的变量的原始值的
    d1.RenameOne("xiaohei")
    d1.Run() // dahuang running

    (&d1).RenameTwo("xiaohei")
    d1.Run() // xiaohei running
}

结构体指针对象调用值接收者方法

当使用结构体指针对象调用值接收者的方法时,Go 编译器会自动将指针对象”解引用”为值调用方法

package main

import "fmt"

type Dog struct {
    name string
}

func (d Dog) Run() {
    fmt.Printf("%s running", d.name)
}

func (d Dog) Rename(name string) {
    d.name = name
}

func main() {
    var d1 *Dog = &Dog{"dahuang"}
    (*d1).Rename("xiaohei") // 自动会解引用成为值类型
    (*d1).Run()				// dahuang running
}

结构体对象调用指针接收者方法

当使用结构体对象调用指针接收者的方法时,Go 编译器会自动将值对象”取引用”为指针调用方法

package main

import "fmt"

type Dog struct {
    name string
}

func (d Dog) Run() {
    fmt.Printf("%s running", d.name)
}

func (d *Dog) Rename(name string) {
    d.name = name
}

func main() {
    var d1 Dog = Dog{"dahuang"}
    d1.Rename("xiaohei") // 自动会取引用成为指针类型
    d1.Run()             // xiaohei running
}

注:取引用和解引用发生在接收者中,对于函数/方法的参数必须保持变量类型一一对

一般使用规则

该使用值接收者还是指针接收者,取决于是否现需要修改原始结构体

  • 若不需要修改则使用值,若需要修改则使用指针
  • 若存在指针接收者,则所有方法使用指针接收者

    对于接收者为指针类型的方法,需要注意在运行时若接收者为 nil 用会发生错误

带方法的结构体命名嵌入

带方法的结构体命名嵌入,使用链式调用嵌入结构体上的方法

package main

import "fmt"

type User struct {
    name string
    addr string
}

func (u *User) SetAddr(addr string) {
    u.addr = addr
}

type Employee struct {
    user   User
    salary float64
}

func (e *Employee) SetSalary(salary float64) {
    e.salary = salary
}

func main() {

    var emp1 Employee = Employee{user: User{name: "jerry", addr: "深圳"}, salary: 3000.00}
    fmt.Println(emp1) // 

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com