Go 语言入门指南:基础语法和常用特性解析 (中)

数组

数组是一种数据类型元素的集合。在Go语言中,数组在声明时就必须确定它的数据类型以及长度,虽然数组在使用时可以修改其数据,但是数组大小无法改变。

数组定义

go 复制代码
var 数组变量名 [元素数量]T

数组在定义时的长度必须是常量,数组的长度也是数组类型的一部分。不同长度的数组是不同类型的。例如[10]int[100]int是不同的类型。

go 复制代码
var a [5]int
var b [50]int

数组初始化

数组必须经过初始化才可以使用。

go 复制代码
func main() {
    var arr1 [3]int //自动初始化,值为0
    arr2 := [3]string{"aa","bb","cc"} //根据值进行初始化
    fmt.Println(arr1) //[0, 0, 0]
    fmt.Println(arr2),//[aa, bb, cc]
}

go语言中,数组的初始化我们也可以不指定长度,我们可以让编译器根据初始值自动推导出数组的长度。

go 复制代码
func main() {
    var arr1 [3]int
    arr2 := [...]int{11, 22, 33, 44, 55}
    fmt.Println(arr1) //[0, 0, 0]
    fmt.Println(arr2) //[11, 22, 33, 44, 55]
    fmt.Println(len(arr2)) //5
}

我们也可以根据索引值,来进行初始化,根据索引值,编译器能够根据最大索引自动推导出数组长度,并且根据初始值来进行初始化数组。

go 复制代码
func main() {
    arr1 := [...]int{1: 111, 3: 333, 5: 555}
    fmt.Println(arr1) //[0, 111, 0, 333, 0, 555]
    fmt.Println(len(arr1)) //6
}

遍历数组

数组遍历我们可以通过for i...的方式或者for range...两种方式进行遍历。

go 复制代码
func main() {
    arr := [...]string{"aaa", "bbb", "ccc", "ddd"}
    
    //通过for i的方式进行遍历
    for i:=0; i<len(arr); i++ {
        fmt.Println(arr[i])
    }
    //aaa
    //bbb
    //ccc
    //ddd
    
    //通过for range的方式进行遍历
    //index表示索引,value表示值
    for index, value := range arr {
        fmt.Println(index, value)
    }
    //0 aaa
    //1 bbb
    //2 ccc
    //3 ddd
    
}

多维数组

go语言中支持多维数组(数组嵌套),我这就拿二维数组进行讲解。

多维数组定义

多维数组在定义上与一维数组几乎一致,只是在内层又嵌套了几层。

go 复制代码
func main() {
    arr1 := [3][2]string{
        {"江苏", "苏州"},
        {"浙江", "杭州"},
        {"福建", "福州"},
    }
    
    fmt.Println(arr1) //[[江苏 苏州] [浙江 杭州] [福建 福州]]
    fmt.Println(arr1[0][1]) //苏州
    
    //多维数组也可以让编译器帮我们自动推导出数组长度,但是仅限在最外层
    arr2 := [...][2]int{
        {11, 111},
        {22, 222},
        {33, 333},
    }
    fmt.Println(arr2) //[[11 111] [22 222] [33 333]]
    fmt.Println(len(arr2), len(arr2[0]) //3 2
}

多维数组遍历

多维数组的遍历与一维数组几乎也一致,但是我们如果需要的是内层的数据,那么我们需要由外往内逐层遍历。

go 复制代码
func main() {
    arr := [3][2]string{
        {"江苏", "苏州"},
        {"浙江", "杭州"},
        {"福建", "福州"},
    }
    for _, row := range arr {
        for _, val := range row {
            fmt.Printf("%v\t", val)
        }
        fmt.Println()
    }
    //江苏 苏州
    //浙江 杭州
    //福建 福州
}

切片

go语言中的切片与数组看似很像,实则却不一样。

  1. 数组的长度是固定的,而切片长度是可变的;切片可以根据需求动态增加切片长度或者缩减切片长度。
  2. 数组是值类型,当进行赋值或者传递时,会复制整个数组;而切片是引用类型,在赋值或者传递时,只会复制指向切片底层数组的引用(地址)。
  3. 数组在声明时就会分配好固定大小的连续空间;而切片则会通过指向底层数组的指针、长度和容量来引用一段连续内存空间。
  4. 数组的索引是固定从0开始到长度-1;而切片可以进行多种灵活的切片操作,从而获取指定范围的子切片。
  5. 数组的长度也是数组类型的一部分,无法改变;而切片的长度也是动态的,我们可以通过len()获取并改变其长度。

当然,具体的使用还需要根据具体的场景而定。切片的灵活性使其更适合出现在动态集合的操作上,而数组更适用于大小固定的、性能要求较高的场景下。

切片定义

切片在定义时,不需要指定长度。

go 复制代码
var 变量名 []变量类型

//声明切片类型

func main() {
    var a []string //声明一个字符串切片
    b := []int{} //声明一个整型切片并进行初始化
    c := []int{11, 22, 33} //声明一个整型切片并进行初始化
    d := []bool{true, false} //声明一个布尔型切片并进行初始化
    fmt.Println(a) //[]
    fmt.Println(b) //[]
    fmt.Println(c) //[11 22 33]
    fmt.Println(d) //[false true]
    fmt.Println(a==nil) //true
    fmt.Println(b==nil) //false
    fmt.Println(c==nil) //false
    fmt.Println(d==nil) //false
}

切片表达式

我们可以通过len()来获取切片的长度,也可以使用cap()来获取切片的容量。

切片表达式有两种,一种是只指定lowhigh两个索引界限值,还有一种是除了lowhigh还要指定容量。

通过上面的学习,我们了解到了切片的底层也是数组,因此我们可以通过切片表达式来得到切片。切片表达式中的lowhigh表示索引范围(左包含,右不包含)。而得到的新切片的长度=high-low,而新切片的容量=原切片的长度-low

go 复制代码
func main() {
    slice := []int{11,22,33,44,55}
    
    s1 := slice[2:3]
    fmt.Println(s1, len(s1), cap(s1))
    //[33] 1 3
    
    s2 := slice[2:]
    fmt.Println(s2, len(s2), cap(s2))
    //[33 44 55] 3 3
    
    s3 := slice[:3]
    fmt.Println(s3, len(s3), cap(s3))
    //[11 22 33] 3 5
    
    s4 := slice[:]
    fmt.Println(s4, len(s4), cap(s4))
    //[11 22 33 44 55] 5 5
    
    s6 := slice[1:2:3] //注意 指定的容量不能小于切片长度,也不能大于原切片的容量
    fmt.Println(s6, len(s6), cap(s6))
    //[22] 1 3
}

通过make()函数构造切片

以上方式我们都是基于数组来创建切片,如果我们需要动态的创建一个切片,我们需要使用make()函数。这里我们需要注意的是,我们分配的容量可能远大于切片长度,但是这并不影响当前切片的使用。

go 复制代码
make([]元素类型, 切片长度, 切片容量)

func main() {
    a := make([]int, 2, 20)
    fmt.Println(a, len(a), cap(a)) //[0 0] 2 20
}

切片的本质其实就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度以及切片的容量。

切片的判断

要判断切片是否为空,需要使用len(slice)==0来进行判断。

这里我们需要知道一点,声明的切片在未进行初始化时,它为nil,当进行初始化后,它不为nil。同时一个nil值的切片的长度和容量都是0,但是我们不能说一个长度和容量为0的切片一定是nil

切片的赋值拷贝

切片的底层是是一个指向数组的指针,所以当我们拷贝当前切片时,对拷贝后的切片进行修改,也会导致当前切片的改变。我们可以将它们理解为指向同一地址的两个指针。

go 复制代码
func main() {
    s1 := make([]int, 3) //[0 0 0]
    s2 := s1 //将s1直接赋值给s2,使得s2和s1指向同一地址
    s2[1] = 111
    fmt.Println(s1) //[0 111 0]
    fmt.Println(s2) //[0 111 0]
}

切片遍历

切片的遍历与数组几乎一致,支持for i...遍历和for range...遍历。

go 复制代码
func main() {
    s := []int{11,22,33}
    
    for i:=0; i<len(s); i++ {
        fmt.Println(i, s[i])
    }
    //1 11
    //2 22
    //3 33
    
    for index, value := range s {
        fmt.Println(index, value)
    }
    //1 11
    //2 22
    //3 33
}

append()切片添加元素

go语言中的append()函数可以动态的为切片添加元素。一次可以添加一个元素,也可以添加多个元素,还可以在另一个切片中的元素(在另一个切片后加...)。

通过var声明的零值切片可以直接在append()函数中使用,不需要进行初始化。

go 复制代码
func main() {
    var s1 []int
    s1 = append(s1, 11) //[11]
    s1 = append(s1, 22, 33) //[11 22 33]
    s2 := []int{44, 55}
    s1 = append(s1, s2...) //[11 22 33 44 55]
}

这时我们就会发现一件事,当我们的切片添加到大于切片容量会怎么办?在切片底层,它会自动进行"扩容","扩容"操作一般发送在append()函数中,所以我们需要用原变量去接收append()的返回值。

切片中的"扩展"发生在切片底层数组长度已经到达当前切片容量时,它会自动进行扩容,在容量小于1024的情况下,每次扩容后的容量都是扩展前的容量的两倍。如果我们并未进行初始化切片的容量,那么切片的容量会按照1,2,4,8...这样的规则进行扩容。如果此时我们给切片进行初始化10的容量,当容量已满时,它会自动扩容为20,40,80...这样的规则。

go 复制代码
func main() {
   var s1 []int
   fmt.Println(cap(s1)) //0
   s1 = append(s1, 1)
   fmt.Println(cap(s1)) //1
   s1 = append(s1, 1)
   fmt.Println(cap(s1)) //2
   s1 = append(s1, 1)
   fmt.Println(cap(s1)) //4

   s2 := make([]int, 3)
   fmt.Println(cap(s2)) //3
   s2 = append(s2, 1,1,1)
   fmt.Println(cap(s2)) //6
}

当容量大于等于1024的情况下,每次扩展仅会扩容原先的四分之一。例如当前容量为1024,那么扩容后的容量为1281(1024+1024*1/4)

copy()复制切片

由于切片是引用类型,所以当我们将原切片赋值给新切片时,它们还是指向同一个地址,当我们修改新切片时原切片也会跟着修改。

那么当我们需要用到原切片的数据,又不想改变原切片时,我们该怎么办呢?

go语言为我们提供了copy()函数,用于将一个切片的数据复制到另一个切片中。

go 复制代码
copy(数据来源切片,目标切片)

func main() {
    a := []int{1,2,3,4,5}
    b := make([]int, 5, 5)
    copy(a, b) //将a中的数据复制到b中
    fmt.Println(a) //[1 2 3 4 5]
    fmt.Println(b) //[1 2 3 4 5]
    b[3] = 33
    fmt.Println(a) //[1 2 3 4 5]
    fmt.Println(b) //[1 2 33 4 5]
}

删除切片元素

go语言中并未提供删除切片元素的专用方法,但是我们可以通过切片本身的特性来删除元素。

go 复制代码
删除切片a中索引为index的元素
append(a[:index], a[index+1]...)

func main() {
    a := []int{11,22,33,44,55}
    a = append(a[:2], a[3:]...)
    fmt.Println(a) //[11 22 44 55]
}

map

map是一种无序的基于key-value的数据结构,go语言中的map是引用类型,必须初始化才可以使用。

map定义

map类型的变量默认初始值为nil, 基本语法:

go 复制代码
map[keyType]valueType

map使用make()来分配内存, map的初始化:

go 复制代码
make(map[keyType]valueType, [cap])

map使用

map中的数据都是成对出现。

go 复制代码
func main() {
    m1 := make(map[string]string, 3)
    m1["江苏"] = "苏州"
    m1["浙江"] = "杭州"
    fmt.Println(m1)
    //map[江苏:苏州 浙江:杭州]

    m2 := map[string]string {
    "name": "zhangsan",
    "age": "18",
    }
    fmt.Println(m2)
    //map[age:18 name:zhangsan]
}

判断键是否存在

go语言中判断键是否存在的特殊写法格式如下:

go 复制代码
value, ok := map[key]

func main() {
    m1 := make(map[string]string, 3)
    m1["江苏"] = "苏州"
    m1["浙江"] = "杭州"
    v, ok := m1["江苏"]
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("查无此地")
    }
}

map遍历

go语言中使用for range...遍历map。

go 复制代码
func main() {
    m1 := make(map[string]string, 3)
    m1["江苏"] = "苏州"
    m1["浙江"] = "杭州"
    for k, v := range m1 {
        fmt.Println(k, v)
    }
    //江苏 苏州
    //浙江 杭州
    
    for k := range m1 {
        fmt.Println(k)
    }
    //江苏
    //浙江
}

delete()删除键值对

go语言中使用delete()来完成map中键值对的删除。

go 复制代码
delete(map, key)

func main() {
    m1 := make(map[string]string, 3)
    m1["江苏"] = "苏州"
    m1["浙江"] = "杭州"
    fmt.Println(m1) //map[江苏:苏州 浙江:杭州]
    delete(m1, "江苏")
    fmt.Println(m1) //map[浙江:杭州]
}

map的嵌套

友情提示,禁止过分套娃!

go 复制代码
func main() {
    //当map的元素为map时
    m1 := make([]map[int]int, 3)
    fmt.Println(m1) //[map[] map[] map[]]
    m1[0] = make(map[int]int, 2)
    fmt.Println(m1) //[map[] map[] map[]]
    m1[0][0] = 11
    m1[0][1] = 22
    fmt.Println(m1) //[map[0:11 1:22] map[] map[]]
    
    //当map的值为切片时
    m2 := make(map[int][]int, 3)
    fmt.Println(m2) //map[]
    m2[1] = make([]int, 5)
    m2[1] = append(m2[0], 11,22,33,44,55)
    fmt.Println(m2) //map[1:[11 22 33 44 55]]
}

函数

函数是一个组织好的,可复用的,用于执行指定任务的代码块。

函数定义

go语言中使用func关键字定义函数。

go 复制代码
func 函数名(参数) (返回值) {
    函数体
}

func add(x int, y int) int {
    return x+y
}

func sayhi(name string) {
    fmt.Println("hi", name)
}

函数调用

定义了函数后,我们可以通过函数名()进行调用函数。

go 复制代码
func add(x, y int) int {
    return x+y
}

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

函数参数

参数简写

当函数参数的相邻变量的类型相同时,我们可以省略类型。

go 复制代码
func add(x, y int) int {
    return x+y
}

可变参数

当参数数量不固定时,我们可以通过在参数名后加...来标识。这里需要注意,可变参数只能作为函数的最后一个参数。

go 复制代码
func sum(x...int) int {
    res := 0
    for _, v := range x {
        res += v
    }
    return res
}

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

函数返回值

go语言中通过return向外输出返回值。

多返回值

go语言中的函数支持多返回值,函数如果有多个返回值时需要用()将所有返回值包裹起来。

go 复制代码
func cal(x, y int) (int, int) {
    sum := x+y
    sub := x-y
    return sum, sub
}

返回值命名

go语言中函数定义时可以给返回值命名,我们在函数体中可以直接使用这些变量,并且最后可以通过return自动返回。

go 复制代码
func cal(x, y int) (sum, sub int) {
    sum := x+y
    sub := x-y
    return
}

变量作用域

全局变量

全局变量定义在函数外部,它在整个程序运行周期内都有效。函数中可以直接访问全局变量。

go 复制代码
package main

import "fmt"

//全局变量
var a = 1

func main() {
    fmt.Println(a)
}

局部变量

局部变量无法在函数外部使用,只能在当前函数内使用。当局部变量与全局变量重名时,优先使用局部变量。当变量定义在iffor中,那么该变量的作用域也仅限于iffor的语句块中。

go 复制代码
package main

import "fmt"

//全局变量
var a = 1

func main() {
    //局部变量
    b := 2
    a := 3
    fmt.Println(b) //2
    fmt.Println(a) //3
}

函数作为参数或返回值

函数作为参数:

go 复制代码
func add(x, y int) int {
    return x+y
}

func cal(x, y int, op func(int, int) int) int {
    return op(x, y)
}

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

函数作为返回值:

go 复制代码
func cal(x, y int, s string) func(int, int) int {
   switch s {
   case "+" :
      return add
   case "-" :
      return sub
   default :
      return nil
   }
}

匿名函数和闭包

匿名函数

当函数作为返回值时,但是go语言函数内部无法像之前那样定义函数,因此只能定义匿名函数,匿名函数就是没有函数名的函数,而且会立即执行。

go 复制代码
func(参数)(返回值){
    函数体
}

func main() {
    //将匿名函数保存到变量中
    add := func(x, y int){
        fmt.Println(x+y)
    }
    add(1,4) //5
    //自动执行函数
    func(x, y int) {
        fmt.Println(x-y)
    }(4, 1) //3
}

闭包

在go语言中,闭包是一种特殊的匿名函数,它可以访问并且操作函数体外部的变量。闭包函数包含了一个函数和一个引用环境,这个引用环境可以是函数体外部定义的变量。闭包=函数+引用环境

变量incr是一个函数,并且它引用了其外部作用域中的sum变量,此时incr就是一个闭包。在incr的生命周期内,变量sum一直有效。

go 复制代码
func main() { 
    //定义一个闭包函数 
    add := func() func(int) int { 
        sum := 0 
        return func(x int) int { 
            sum += x 
            return sum 
        } 
    } 
    
    // 使用闭包函数 
    incr := add() 
    fmt.Println(incr(1)) //1 
    fmt.Println(incr(2)) //3 
    fmt.Println(incr(3)) //6 
}

defer语句

go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理语句按defer定义的逆序进行执行。先被defer的语句最后执行,后被defer的语句最先执行。

go 复制代码
func main() {
    defer fmt.Println(11)
    defer fmt.Println(22)
    defer fmt.Println(33)
    //33
    //22
    //11
}

go语言中defer这一延迟调用的特性,在资源释放问题上非常方便。拿线程举例,每条线程在执行完毕之后我们都需要将其释放,而有了defer这一特性,我们可以在开启线程后就使用defer进行释放线程,当代码全部执行完毕,线程也会跟着释放。

内置函数

内置函数 介绍
close 关闭channel
len 用来求长度
new 用来分配内存。返回的是指针
make 用来分配内存,主要用来分配引用类型。
append 用来追加元素到数组、slice中
panic/recover 错误处理

panic/recover

go语言中没有异常机制,但是可以使用panic/recover来处理错误。panic可以在任何地方使用,但是recover只能在defer调用的函数中有效。

go 复制代码
func func1() {
    fmt.Println("func1")
}

func func2() {
    defer func() {
        err := recover()
        //当程序中出现了panic错误,可以通过recover恢复过来
        if err != nil {
            fmt.Println("recover in func2")
        }
    }()
    panic("panic in func2")
}

func func3() {
    fmt.Println("func3")
}
func main() {
    func1()
    func2()
    func3()
    //func1
    //recover in 2
    //func3
}

指针

这里我们需要先了解指针的三个概念:指针地址、指针类型、指针取值。

任何数据加载进入内存后,都拥有一个它们的地址值,这就是指针。而为了保存这个数据在内存中的地址值,我们就需要指针变量。

指针地址和指针类型

每个变量在运行时都有一个地址值,这个地址代表该变量在内存中的位置。go语言中使用&字符放在变量前对变量进行"取地址"的操作。而go语言中的值类型都有对应的指针类型,指针类型就是在值类型的前面加上*字符,例如:*int*string等。

go 复制代码
指针变量 := &地址变量

func main() {
   a := 1
   b := &a
   fmt.Printf("%v, %p\n", a, &a) //1, 0xc00000e0b8, int
   fmt.Printf("%v, %p\n", b, b) //0xc0000a6058, 0xc0000ca018, *int
}

指针取值

这里我们非常容易混淆指针、地址。对变量取地址(&)可以获得这个变量的指针变量。指针变量的值就是指针地址。对指针变量取值(*)可以获得指针变量指向的原变量的值

go 复制代码
func main() {
    a := 1
    b := &a //取a的地址,保存到b中
    fmt.Printf("%T\n", b) //*int
    c := *b //对指针取值
    fmt.Printf("%T\n", c) //int
    fmt.Printf("%v\n", c) //1
}

new

在go语言中,new函数用于内存分配,但是new出来的变量类型是指针类型,并且该变量的值为该类型的零值。

go 复制代码
func new(Type) *Type

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
    
    //声明一个*int类型的c变量
    var c *int
    //进行初始化后才可以使用
    c = new(int)
    fmt.Println(*c) //0
}

make与new类似,两者都是用于内存分配。但是make只用于slice、map以及channel的初始化,返回类型是这三个引用类型本身。然而new用于类型的内存分配,该内存对应值为该类型的零值,返回类型是该类型的指针类型。

结构体

当我们想表达一个事物的全部或部分属性时,我们无法使用单一的数据类型来满足需求,这个时候我们需要一种自定义数据类型,来完成对该事物的封装。而这种数据类型在go中,被称为结构体,英文名称struct

对于学习过Java的各位小伙伴们一定知道,Java中万物皆对象,然而在go中,我们通过struct来面向对象。

结构体的定义

我们使用typestruct来定义结构体。

类型名表示自定义结构体的名称,并且同一包下不能重复。字段名表示结构体的属性名,在当前结构体内字段名必须唯一。字段类型则是表示字段的具体类型。

go 复制代码
type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    ...
}

我们定义一个学生结构体与老师结构体:

go 复制代码
type student struct {
    name string
    class string
    age int
}

//相同类型字段可以写在同一行
type teacher struct {
    name, course string
    age int
}

目前,我们拥有两个自定义类型,一个学生类型和一个老师类型。学生类型拥有姓名、班级、年龄三个字段;老师类型也拥有姓名、课程、年龄三个字段。如此我们就可以通过该类型来存储学生和老师的信息了。

结构体实例化

当结构体实例化后,才会分配内存空间,才能使用结构体的字段。

结构体也是一种类型,我们可以像声明变量那样声明结构体类型。

go 复制代码
var 结构体实例 结构体类型

type student struct {
    name string
    age int
}

func main() {
    var s1 student
    s1.name = "zhangsan"
    s1.age = 18
    fmt.Printf("%v\n", s1) 
    //{zhangsan 18}
    fmt.Printf("%#v\n", s1) 
    //main.student{name:"zhangsan", age:18}
}

匿名结构体

匿名结构体常用于一些临时的数据结构上。

go 复制代码
func main() {
    var person struct{
        name string; 
        age int
    }
    person.name = "张三"
    person.age = 18
    fmt.Println(person) //{张三 18}
}

指针类型结构体

我们可以通过new来进行结构体的实例化,得到的是结构体的地址。

go 复制代码
type student struct {
    name string
    age int
}

func main() {
    var s1 = new(student)
    fmt.Printf("%T\n", s1) 
    //*main.student
    s1.name = "李四"
    s1.age = 19
    fmt.Printf("%#v\n", s1) 
    //&main.student{name:"李四", age:19}
}

当我们使用&对结构体进行取地址后相当于对该结构体类型又进行了一次new的实例化。

go 复制代码
type student struct {
    name string
    age int
}

func main() {
    s1 := &student{}
    fmt.Printf("%T\n", s1)
    //*main.student
    fmt.Printf("%#v\n", s1)
    //&main.student{name:"", age:0}
    s1.name = "lisi"
    s1.age = 19
    fmt.Printf("%#v\n", s1)
    //&main.student{name:"lisi", age:19}
}

结构体初始化

当我们声明一个结构体后,结构体的字段值都为各字段类型的默认零值,只有当我们进行初始化后才拥有各字段对应的值。

go 复制代码
type student struct {
    name string
    age int
}

func main() {
   s1 := student{}
   fmt.Printf("%#v\n", s1)
   //main.student{name:"", age:0}

   //使用键值对结构体进行初始化
   s2 := student{
      name: "小明",
      age: 20,
   }
   fmt.Printf("%#v\n", s2)
   //main.student{name:"小明", age:20}
   s3 := student{
      name: "小美",
   }
   fmt.Printf("%#v\n", s3)
   //main.student{name:"小美", age:0}

   //使用值的列表进行初始化
   //必须将所有结构体字段按照顺序依次进行初始化
   s4 := &student{
      "小红",
      19,
   }
   fmt.Printf("%#v\n", s4)
   //&main.student{name:"小红", age:19}
}

类型别名和自定义类型

类型别名

大家可以将类型别名理解为小名、外号。虽然名字不同,但是指向的都是同一个类型。

go 复制代码
type byte = uint8
type rune = int32

自定义类型

在go语言中,我们可以使用type来自定义类型,自定义类型其实就是定义了一个全新的类型。

大家可以理解为一对夫妇,生下了一个小男孩,然后为其取名为小明。

go 复制代码
//将MyInt定义为int类型
type MyInt int

自定义类型和类型别名的区别

两者看似只有一个等号的差异。但是类型别名的类型依旧是原类型,而自定义类型的类型是新类型名。

go 复制代码
//自定义类型
type newInt int
//类型别名
type myInt = int

func main() {
    var a newInt
    var b myInt
    fmt.Printf("%T\n", a) //main.newInt
    fmt.Printf("%T\n", b) //int
}

空结构体

当我们声明一个空结构体时,它本身是不占用任何内存空间的。

go 复制代码
func main() {
    var s struct{}
    fmt.Println(unsafe.Sizeof(s)) //0
}

构造函数

学习过Java的小伙伴们一定都知道,当我们需要对一个对象进行初始化时,往往通过其的构造函数进行初始化。而go语言中并没有构造函数,但是我们可以通过函数来自己实现。由于struct是值类型,值拷贝的性能消耗会比较大,所以我们通常使用结构体指针类型作为返回值。

go 复制代码
type student struct {
    name string
    age int
}

func newStudent(name string, age int) *student {
    return &student{
        name: name,
        age: age,
    }
}

func main() {
    s1 := newStudent("zhangsan", 18)
    fmt.Printf("%#v\n", s1)
    //&main.student{name:"zhangsan", age:18}
}

方法和接收者

go语言中的方法是一种作用于特点类型变量的函数。这种特定变量叫做接收者。接收者分为指针类型接收者和值类型接收者两种。当需要修改接收者的属性时,一般使用指针类型接收者;当接收者占用内存过大时,一般也使用指针类型接收者。

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

type student struct {
    name string
    age int
}

func newStudent(name string, age int) *student {
    return &student{
        name: name,
        age: age,
    }
}

func (s student) sleep() {
    fmt.Printf("%v is sleeping in class\n", s.name)
}

func main() {
    s1 := newStudent("zhangsan", 18)
    s1.sleep() //zhangsan is sleeping in class
}

指针类型接收者

指针类型接收者常用于修改结构体中的字段时,例如需要给结构体字段设值的时候。修改后的值在整个方法内都有效,直到整个方法结束为止。

go 复制代码
type student struct {
    name string
    age int
}

func newStudent(name string, age int) *student {
    return &student{
        name: name,
        age: age,
    }
}

func (s *student) setAge(newAge int) {
    s.age = newAge
}

func main() {
    s1 := newStudent("zhangsan", 18)
    fmt.Println(s1) //&{zhangsan 18}
    s1.setAge(20)
    fmt.Println(s1) //&{zhangsan 20}
}

值类型接收者

当方法作用于值类型接收者时,go语言会将接收者的值复制一份。在值类型接收者的方法中可以获取该接收者的成员值,但是只能在当前方法中使用。常用于结构体的行为方法。

go 复制代码
type student struct {
    name string
    age int
}

func newStudent(name string, age int) *student {
    return &student{
        name: name,
        age: age,
    }
}

func (s *student) setAge(newAge int) {
    s.age = newAge
}

func (s student) sleep() {
    fmt.Printf("%v is sleeping in class\n", s.name)
}

func main() {
    s1 := newStudent("zhangsan", 18)
    fmt.Println(s1) //&{zhangsan 18}
    s1.setAge(20)
    fmt.Println(s1) //&{zhangsan 18}
    s1.sleep() //zhangsan is sleeping in class
}

任意类型添加方法

在go语言中,接收者的类型可以是任何类型,不仅是结构体,所有类型都可以添加方法。但是有个前提条件,该类型必须是自定义类型。

go 复制代码
type myInt int

func (m myInt) say() {
    fmt.Println("myInt 的自定义方法")
}

func main() {
    var m1 myInt
    m1.say()
    //myInt 的自定义方法
}

结构体的匿名字段

在结构体声明时,字段名可以只声明类型,而没有字段名。

go 复制代码
type student struct {
    string
    int
}

func main() {
    s1 := student{
        "zhangsan",
        19,
    }
    fmt.Printf("%#v\n", s1) 
    //main.student{string:"zhangsan", int:19}
}

结构体嵌套

一个结构体中可以嵌套另外一个结构体。

go 复制代码
type address struct {
    province string
    city string
}

type person struct {
    name string
    age int
    address address
}

func main() {
    p1 := person{
        name: "张三",
        age: 19,
        address: address{
            province: "江苏",
            city: "苏州",
        },
    }
    fmt.Printf("%#v\n", p1)
    //main.person{name:"张三", age:19, address:main.address{province:"江苏", city:"苏州"}}
}

嵌套匿名函数

同样的,我们也可以在结构体中嵌套匿名字段。

go 复制代码
type address struct {
    province string
    city string
}

type person struct {
    name string
    age int
    address //匿名字段
}

func main() {
    var p1 person
    p1.name = "张三"
    p1.age = 19
    p1.address.province = "江苏" //默认使用类型名作为字段名
    p1.city = "苏州" //匿名字段可以省略
    fmt.Printf("%#v\n", p1)
    //main.person{name:"张三", age:19, address:main.address{province:"江苏", city:"苏州"}}
}

嵌套结构体字段名冲突

当嵌套结构体的字段名和外部结构体的字段名冲突时,这个时候我们就需要指定具体的结构体字段名来指明字段。

go 复制代码
type address struct {
   time string
}

type email struct {
   time string
}

type person struct {
   time string
   address //匿名字段
   email //匿名字段
}

func main() {
   var p1 person
   p1.time = "2023"
   fmt.Println(p1) //{2023 {} {}}
   p1.address.time = "2022"
   fmt.Println(p1) //{2023 {2022} {}}
   p1.email.time = "2021"
   fmt.Println(p1) //{2023 {2022} {2021}}
}

结构体继承

go语言可以通过嵌套匿名结构体来实现其他语言中的面向对象的继承。

go 复制代码
type animal struct {
    name string
}

func (a *animal) eat() {
    fmt.Printf("%v正在吃东西\n", a.name)
}

type dog struct {
    age int
    *animal
}

func (d *dog) bark() {
    fmt.Printf("%v岁的%v正在叫\n", d.age, d.name)
}

func main() {
   d1 := &dog{
      age: 3,
      animal: &animal{ //嵌套结构体指针
         name: "小白",
      },
   }
   d1.eat() //小白正在吃东西
   d1.bark() //3岁的小白正在叫
}

可见性

go语言中没有所谓的关键字来表示公有和私有,在go语言中,结构体字段首字母大写就表示可公开访问,小写表示私有。

小结

有了上篇的基础,我们又从本文中认识到go语言中的数组、切片、map集合、函数、指针类型以及结构体。希望小伙伴们能够有所收获。到目前为止,go语言的基础语法我们已经大致掌握,但是go语言的基础不止这些。敬请收看下篇《Go 语言入门指南:基础语法和常用特性解析 (下) 》。

码字不易,如果您看到了这里,听我说谢谢您。

如果您觉得本文还不错,还请留下您小小的赞。

如果您看了本文有所感受,还请留下您宝贵的评论。

相关推荐
CallBack8 个月前
Typora+PicGo+阿里云OSS搭建个人图床,纵享丝滑!
前端·青训营笔记
Taonce1 年前
站在Android开发者的角度认识MQTT - 源码篇
android·青训营笔记
AB_IN1 年前
打开抖音会发生什么 | 青训营
青训营笔记
monster1231 年前
结营感受(go) | 青训营
青训营笔记
翼同学1 年前
实践记录:使用Bcrypt进行密码安全性保护和验证 | 青训营
青训营笔记
hu1hu_1 年前
Git 的正确使用姿势与最佳实践(1) | 青训营
青训营笔记
星曈1 年前
详解前端框架中的设计模式 | 青训营
青训营笔记
tuxiaobei1 年前
文件上传漏洞 Upload-lab 实践(中)| 青训营
青训营笔记
yibao1 年前
高质量编程与性能调优实战 | 青训营
青训营笔记
小金先生SG1 年前
阿里云对象存储OSS使用| 青训营
青训营笔记