Go 语言入门指南:基础语法和常用特性解析 | 青训营(下)

269 阅读17分钟

Go语言基础语法

Go 语言入门指南:基础语法和常用特性解析 | 青训营(上)

上一篇笔记点此链接 ↑↑↑

第二节:入门

2.7 指针

指针在Go语言中也是一个重要的概念。指针存储了一个变量的内存地址,它用于在函数间传递数据、修改数据等场景。

package main

import "fmt"

func main() {
    num := 42
    ptr := &num // 创建指向num的指针
    
    fmt.Println(*ptr) // 输出指针所指向的值:42
}

Go 拥有指针。指针保存了值的内存地址。 类型*T是指向T类型值的指针。其零值为nil

var p *int

& 操作符会生成一个指向其操作数的指针。

i := 42
p = &num
  • 操作符表示指针指向的底层值。
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21         // 通过指针 p 设置 i

这也就是通常所说的“间接引用”或“重定向”。 与 C 不同,Go 没有指针运算。

2.8 结构体

结构体(Struct)是一种自定义的数据类型,用于封装不同类型的数据。可以将结构体看作是一种面向对象的轻量级实现。

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    person := Person{Name: "Alice", Age: 30}
    fmt.Println(person.Name, person.Age) // 输出 Alice 30
}

结构体字段用.去访问,如person.Namepersion.Age

当然结构体字段也可以通过结构体指针来访问。

v := Person{1, 2}
p := &v
p.X = 1e9
fmt.Println(v) // 输出{1000000000 2}

如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以。

2.9 数组

在 Go 语言中,数组是一种用于存储一组固定大小元素的数据结构。数组的大小在创建时就已经确定,无法改变。下面是关于 Go 语言中数组的一些基本信息:

声明数组

在 Go 中,声明一个数组需要指定数组的类型和大小。语法如下:

var arrayName [size]dataType

其中,arrayName 是数组的名称,size 是数组的大小,dataType 是数组中存储的数据类型。

例如,声明一个包含 5 个整数的数组:

var numbers [5]int

初始化数组

可以使用花括号 {} 来初始化数组的元素。如果在声明时同时初始化数组,可以省略数组的大小。

var numbers = [5]int{1, 2, 3, 4, 5}

如果省略了数组的大小,Go语言会根据初始化的元素数量推断数组的大小,但同时也构建了一个引用它的切片:

var numbers = []int{1, 2, 3, 4, 5}

访问数组元素

通过下标(从 0 开始)来访问数组中的元素。

numbers[0] // 第一个元素
numbers[2] // 第三个元素

数组长度

可以使用 len() 函数获取数组的长度。

length := len(numbers) // 数组 numbers 的长度

多维数组

Go 语言也支持多维数组,例如二维数组:

var matrix [3][3]int // 声明一个 3x3 的二维数组

matrix = [3][3]int{
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9},
}

注意事项

  • Go 中的数组是值类型,当将一个数组赋值给另一个数组时,会复制数组的所有元素。
  • 数组的大小是其类型的一部分,因此 [5]int[10]int 是不同的类型。
  • 在实际编程中,切片(Slice)更为常用,因为切片可以动态调整大小,而数组大小是固定的。

虽然数组在 Go 中的使用有一些限制,但切片和其他数据结构可以更好地满足动态大小的需求。

2.10 切片

在 Go 语言中,切片(Slice)是一种动态数组,它是基于数组的抽象,可以方便地操作和管理可变长度的序列数据。切片提供了更灵活、更强大的方式来处理数据集合,相较于固定长度的数组,切片可以根据需要自动扩展或缩小。

以下是关于 Go 语言中切片的一些基本信息:

声明切片

要声明一个切片,可以使用如下的语法:

var sliceName []elementType

其中,sliceName 是切片的名称,elementType 是切片中元素的数据类型。

例如,声明一个存储整数的切片:

var numbers []int

创建切片

可以通过对数组、切片或其他切片进行切割来创建一个新的切片。切片使用的是数组的一部分作为其底层数据。

arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // 创建一个包含 arr[1], arr[2], arr[3] 的切片

初始化切片

切片可以在声明时初始化,也可以在之后通过 append() 函数添加元素来动态初始化。

slice := []int{1, 2, 3}

切片的长度和容量

切片有两个重要的属性:长度(len(slice))和容量(cap(slice))。切片的长度是当前切片中元素的个数,容量是从切片的起始元素到底层数组的最后一个元素的个数。

切片的操作

Go 语言提供了丰富的切片操作方法,包括获取元素、追加元素、切割切片、拷贝切片等。

slice[0] // 获取切片的第一个元素
slice = append(slice, 4) // 向切片末尾添加元素
newSlice := slice[1:3] // 切割切片,包含 slice[1] 和 slice[2]
copy(newSlice, slice) // 拷贝切片到新的切片

切片的引用和底层数组

切片是引用类型,多个切片可以引用相同的底层数组。修改一个切片的元素会影响到底层数组,因此其他引用同一底层数组的切片也会受到影响。

注意事项

  • 当通过切片创建新的切片时,新切片和原始切片共享相同的底层数组,修改其中一个会影响另一个。
  • 使用 make() 函数可以创建指定大小和容量的切片,避免因底层数组大小不足而频繁扩容。
  • 切片不需要指定长度,其长度由实际元素个数决定。

nil切片

切片的零值是 nil。 nil 切片的长度和容量为 0 且没有底层数组。

package main

import "fmt"

func main() {
    var s []int
    fmt.Println(s, len(s), cap(s)) //输出[],0,0
    if s == nil {
        fmt.Println("nil!") //输出nil!
    }
}

用make创建切片

切片可以用内建函数make来创建,这也是创建动态数组的方式。

package main

import "fmt"

func main() {
    a := make([]int, 5)
    printSlice("a", a) // a, len=5, cap=5, [0 0 0 0]

    b := make([]int, 0, 5)
    printSlice("b", b) // b, len=0, cap=5, []

    c := b[:2]
    printSlice("c", c) // c, len=2, cap=5, [0 0]

    d := c[2:5]
    printSlice("d", d) // d, len=3, cap=3, [0 0 0]
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x) // 分别输出这个切片的变量名,长度,容量,切片的内容
}

make函数会分配一个元素为零值的数组并返回一个引用了它的切片,这和数组的声明很想:

a := make([]int, 5)

要指定它的容量,则需传入第三个参数:

b := make([]int, 0, 5) //len(b)=0, cap(b)=5

向切片追加元素

为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append函数。内建函数的文档对此函数有详细的介绍。

var s []int //nil切片
s = append(s, 0) //添加一个元素,这是len=1,cap=1,[0]
s = append(s, 2, 3, 4) //添加多个元素

总的来说,切片在 Go 语言中非常实用,它提供了灵活的数据结构来处理可变长度的序列数据,是处理集合和数据集的重要工具。

2.11 函数

函数是Go语言的基本构建块之一,通过func关键字定义。函数可以有参数和返回值,也可以是匿名函数。以下是一个简单的示例:

package main

import "fmt"

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

func main() {
    result := add(5, 3)
    fmt.Println(result) // 输出 8
}

2.12 Map集合(映射)

在 Go 语言中,映射(Map)是一种集合类型。映射提供了一种从键到值的快速查找方式,类似于其他编程语言中的字典或关联数组。每个键只能在映射中出现一次,但是对应的值可以是不同的。

创建映射

// 使用 make 函数创建一个空映射
m := make(map[keyType]valueType)

// 创建并初始化映射
person := map[string]int{
    "Alice":   25,
    "Bob":     30,
    "Charlie": 28,
}

其中 keyType 是键的数据类型,valueType 是值的数据类型。

添加和修改元素

person["David"] = 22 // 添加键为 "David",值为 22 的元素
person["Alice"] = 26 // 修改键为 "Alice" 的元素值为 26

删除元素

delete(person, "Bob") // 删除键为 "Bob" 的元素

获取元素值

age := person["Alice"] // 获取键为 "Alice" 的值,如果键不存在,则返回零值

检查键是否存在

age, exists := person["Bob"] // 如果键存在,exists 为 true,否则为 false

遍历映射

for key, value := range person {
    fmt.Printf("Name: %s, Age: %d\n", key, value)
}

映射的长度

length := len(person) // 获取映射中键值对的数量

映射是一种引用类型,当传递映射给函数或将映射赋值给另一个变量时,它们共享底层的数据。因此,对映射的修改会影响所有引用它的地方。

需要注意的是,映射是无序的,即遍历映射的元素顺序与添加元素的顺序无关。

映射在 Go 语言中是非常常用且强大的数据结构,适用于需要根据键快速查找对应值的场景。

2.13 range

在 Go 语言中,range 关键字用于遍历各种数据结构,如数组、切片、映射(map)、通道(channel)等。range 在循环中提供了每个元素的副本,从而可以遍历数据结构中的每个元素。以下是 range 关键字的一些常见用法:

遍历数组或切片

numbers := []int{1, 2, 3, 4, 5}
for index, value := range numbers {
    fmt.Printf("Index: %d, Value: %d\n", index, value)
}

在上述示例中,range numbers 遍历切片 numbers 中的每个元素,并将索引和值分别赋值给 indexvalue

遍历映射(map)

person := map[string]int{"Alice": 25, "Bob": 30, "Charlie": 28}
for key, value := range person {
    fmt.Printf("Name: %s, Age: %d\n", key, value)
}

在这个示例中,range person 遍历映射 person 中的每个键值对,并将键和值分别赋值给 keyvalue

注意事项

  • 当使用 range 遍历切片、数组、映射或通道时,每次循环迭代都会生成元素的副本。这意味着在循环中修改元素的值不会影响原始数据结构。
  • 如果只需要索引而不需要值,可以使用下划线 _ 来省略变量的声明。
  • range 可以用于任何实现了迭代器(Iterator)模式的数据结构。
  • 对于切片和数组,range 会生成索引和值;对于映射,会生成键和值;对于通道,只会生成值。

总的来说,range 是一个方便且强大的特性,用于遍历多种数据结构,让循环更加简洁和易读。

2.14 字符串操作

Go语言中的字符串是不可变的,但通过标准库提供的字符串操作函数,可以进行各种操作,如拼接、分割等。

package main

import "fmt"

func main() {
    str1 := "Hello"
    str2 := "World"
    result := str1 + " " + str2
    fmt.Println(result) // 输出 Hello World
}

2.15 接口

接口(Interface)用于定义一组方法的签名。接口提供了一种方式来描述对象应该具备的行为,而不需要关心具体的实现细节。接口允许不同的类型实现相同的方法集合,从而实现多态性。

定义接口

// 定义一个接口
type Shape interface {
    Area() float64
    Perimeter() float64
}

在上面的例子中,Shape 是一个接口,它包含了两个方法 Area()Perimeter() 的签名。任何类型只要实现了这两个方法,就被视为实现了 Shape 接口。

实现接口

// 定义一个结构体类型
type Rectangle struct {
    Width  float64
    Height float64
}

// 让 Rectangle 类型实现 Shape 接口的方法
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2*r.Width + 2*r.Height
}

在上述例子中,Rectangle 结构体实现了 Shape 接口的 Area()Perimeter() 方法。通过在结构体上定义方法,该结构体就自动实现了接口中定义的方法。

使用接口

func PrintShapeDetails(s Shape) {
    fmt.Printf("Area: %f, Perimeter: %f\n", s.Area(), s.Perimeter())
}

在这个例子中,我们定义了一个函数 PrintShapeDetails,它接受一个参数类型为 Shape 接口的对象。无论传入的具体类型是什么,只要它实现了 Shape 接口的方法,都可以被传递给这个函数。

接口类型的变量

var s Shape
s = Rectangle{Width: 3, Height: 4}
fmt.Println(s.Area())      // 调用 Rectangle 的 Area 方法
fmt.Println(s.Perimeter()) // 调用 Rectangle 的 Perimeter 方法

在这个例子中,我们可以将实现了接口的类型赋值给接口类型的变量。通过接口变量,我们可以调用接口中定义的方法,而不需要知道实际的类型。

接口在 Go 语言中用于实现多态性和抽象。通过接口,可以定义通用的行为规范,让不同的类型实现这些规范,从而实现代码的灵活性和可扩展性。

2.16 错误处理

Go语言使用error类型来表示错误。函数返回值通常包括一个结果和一个错误,通过检查错误来确定操作是否成功。

package main

import (
    "fmt"
    "errors"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除数不能为0")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println

("错误:", err)
    } else {
        fmt.Println("结果:", result)
    }
}

2.17 JSON和时间数据

在 Go 语言中,处理 JSON 数据和时间数据都是非常常见的任务。以下是关于在 Go 中处理 JSON 数据和时间数据的基本知识:

JSON 数据处理

编码(序列化)

Go 语言中的 encoding/json 包提供了将 Go 数据结构编码为 JSON 格式的功能。你可以使用 json.Marshal() 函数来将 Go 对象序列化为 JSON 字符串。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Email string `json:"email,omitempty"`
}

func main() {
    person := Person{Name: "Alice", Age: 25}
    jsonData, err := json.Marshal(person)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("JSON:", string(jsonData))
}

在上述示例中,我们定义了一个 Person 结构体,并使用 json:"key" 标签来指定 JSON 字段的名称。然后,我们使用 json.Marshal() 函数将 Person 结构体编码为 JSON 字符串。

解码(反序列化)

要将 JSON 字符串解码为 Go 数据结构,可以使用 json.Unmarshal() 函数。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Email string `json:"email,omitempty"`
}

func main() {
    jsonStr := `{"name":"Bob","age":30}`
    var person Person
    err := json.Unmarshal([]byte(jsonStr), &person)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Name:", person.Name)
    fmt.Println("Age:", person.Age)
}

在上述示例中,我们将一个 JSON 字符串解码为 Person 结构体对象,并访问了其中的字段。

时间数据处理

Go 语言中的 time 包提供了处理时间和日期的功能。

package main

import (
    "fmt"
    "time"
)

func main() {
    currentTime := time.Now()
    fmt.Println("Current Time:", currentTime)

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

    // 解析时间
    parsedTime, err := time.Parse("2006-01-02 15:04:05", "2023-08-15 12:30:00")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Parsed Time:", parsedTime)
}

在上述示例中,我们使用 time.Now() 获取当前时间,使用 Format() 方法将时间格式化为字符串,使用 Parse() 方法将字符串解析为时间对象。

总之,Go 语言中的 encoding/json 包和 time 包提供了强大的工具来处理 JSON 数据和时间数据,使得在处理这些常见任务时变得更加简便。

第三节:并发

Go语言的并发支持是其显著的特点,它允许程序以高效且简洁的方式处理并发任务,提高程序性能和效率。并发的核心是协程(goroutine)和通道(channel)。 并发是同时执行多个任务的能力,Go 语言通过协程(goroutine)和通道(channel)的机制来实现并发编程,使得编写并发代码变得更加简单且高效。

协程(goroutine)

协程是 Go 语言中用于并发执行任务的轻量级线程。与传统的线程相比,协程的创建和销毁成本较低,可以在单个操作系统线程上运行许多协程。使用协程可以充分利用多核处理器,实现更高效的并发。

在 Go 语言中,可以使用 go 关键字创建一个新的协程。以下是一个简单的示例:

package main

import (
    "fmt"
    "time"
)

func printNumbers() {
    for i := 1; i <= 5; i++ {
        fmt.Println(i)
        time.Sleep(time.Millisecond * 500) //时间间隔为500毫秒
    }
}

func printLetters() {
    for i := 'a'; i <= 'e'; i++ {
        fmt.Println(string(i))
        time.Sleep(time.Millisecond * 400)
    }
}

func main() {
    go printNumbers()
    go printLetters()

    time.Sleep(time.Second * 3)
}

在上述示例中,我们创建了两个协程 printNumbersprintLetters,分别输出数字和字母。通过使用 go 关键字,这两个函数可以并发地执行。为了确保主函数等待协程执行完成,我们使用了 time.Sleep()

通道(channel)

在 Go 语言中,通道(channel)是用于协程之间通信的一种基本机制。通道允许一个协程发送数据,另一个协程接收数据,从而实现协程之间的同步和数据交换。通道是 Go 语言并发编程中非常重要的组成部分,用于避免竞态条件、协程之间的协调以及数据共享。

创建通道

可以使用 make() 函数创建通道。通道有两种类型:带缓冲的通道和非缓冲的通道。缓冲通道允许在发送数据时不阻塞,直到通道已满;而非缓冲通道在发送数据和接收数据时都会阻塞,直到另一方就绪。

创建带缓冲的通道

ch := make(chan int, 10) // 创建一个可容纳 10 个整数的缓冲通道

创建非缓冲的通道

ch := make(chan int) // 创建一个非缓冲通道

发送和接收数据

在通道中,使用 <- 运算符进行数据的发送和接收操作。

发送数据到通道

ch <- value // 发送 value 到通道 ch

从通道接收数据

value := <-ch // 从通道 ch 接收数据,并将其赋值给变量 value

关闭通道

可以使用 close() 函数关闭一个通道,关闭通道后不再可以发送数据,但可以继续接收已经在通道中的数据。

close(ch) // 关闭通道 ch

循环遍历通道

使用 range 关键字可以循环遍历通道中的数据。

for value := range ch {
    // 处理接收到的数据 value
}

示例

以下是一个简单的示例,展示了如何使用通道进行协程之间的数据传输:

package main

import (
    "fmt"
    "time"
)

func sender(ch chan int) {
    for i := 1; i <= 5; i++ {
        fmt.Println("Sending:", i)
        ch <- i
        time.Sleep(time.Millisecond * 500)
    }
    close(ch)
}

func main() {
    ch := make(chan int, 2) // 创建一个带缓冲的通道

    go sender(ch) // 启动协程发送数据

    for value := range ch {
        fmt.Println("Received:", value)
    }
}

在上述示例中,我们创建了一个带缓冲的通道 ch,然后在 sender 协程中发送数据到通道中,主函数中通过 range 循环遍历通道中的数据并接收。

通道是 Go 语言中协程通信的基础,它能够有效地管理并发操作,避免了竞态条件和数据不一致性的问题。

Go 语言提供了强大的协程和通道机制,使得并发编程变得更加容易和高效。通过合理的使用协程和通道,可以充分利用多核处理器,提高程序的性能和响应能力。

以上就是Go语言中所有的基础语法和常用特性了。感谢大家阅读。

参考文件:Go指南