Go语言基础学习详细笔记

发布于:2025-05-10 ⋅ 阅读:(17) ⋅ 点赞:(0)

初步了解Go语言

Go语言诞生的主要问题和目标

在这里插入图片描述

多核硬件架构: 随着计算机硬件的发展,多核处理器成为主流,使得并行计算变得普遍。然而,传统的编程语言在处理多核并行性时可能面临困难,因为它们缺乏合适的原生支持。Go语言通过引入轻量级的协程(goroutine)和通道(channel)机制,使得并发编程变得更加容易。开发者可以轻松地创建数千个并发执行的协程,而无需担心线程管理的复杂性。

超大规模分布式计算集群: 随着云计算和分布式系统的崛起,构建和维护超大规模的分布式计算集群变得越来越常见。这些集群需要能够高效处理大量的请求、数据共享和协调。Go语言的并发特性和通道机制使得编写分布式系统变得更加容易,开发者可以使用协程和通道来处理并发任务、消息传递和协调工作。

Web模式导致的开发规模和更新速度增加: Web应用的兴起带来了前所未有的开发规模和持续更新的需求。传统的编程语言在开发大型Web应用时可能会面临可维护性、性能和开发效率等问题。Go语言通过其简洁的语法、高效的编译速度以及并发支持,使得开发者能够更快速地迭代和部署Web应用,同时也能够更好地处理高并发的网络请求。

综合来看,Go语言在诞生时确实着重解决了多核硬件架构、超大规模分布式计算集群和Web模式下的开发规模与速度等技术挑战,它的设计目标之一是提供一种适应现代软件开发需求的编程语言,使开发者能够更好地应对这些挑战。

Go语言应用典型代表

Go语言在当下应用开发中已经得到广泛应用,许多知名公司和项目都使用Go语言来构建各种类型的应用。以下是一些代表性的产品和项目,它们使用了Go语言作为核心开发语言:

这些仅仅是Go语言应用的一小部分示例,实际上还有许多其他的项目和产品也在使用Go语言来构建高性能、可靠且易于维护的应用程序。这表明Go语言在现代应用开发中发挥了重要作用,特别是在分布式系统、云计算和高性能应用领域。

Go语言开发环境搭建

请参考该文章进行搭建:
Go语言开发环境搭建

经典HelloWorld

src目录下新建hello.go文件,编写代码如下:

package main

import (
	"fmt"
	"os"
)

func main() {
	if len(os.Args) > 1 {
		fmt.Println("hello world", os.Args[1])
	}
}

这段代码是一个简单的Go语言程序,它接受命令行参数并打印出一条带参数的 “Hello World” 消息。下面是对代码的逐行分析:

package main: 声明这个文件属于名为 “main” 的包,这是一个Go程序的入口包名。

import ("fmt" "os"): 引入了两个标准库包,分别是 “fmt” 用于格式化输出和 “os” 用于与操作系统交互。

func main() { ... }: 这是程序的入口函数,它会在程序运行时首先被调用。

if len(os.Args) > 1 { ... }: 这个条件语句检查命令行参数的数量是否大于1,也就是判断是否有参数传递给程序。os.Args 是一个字符串切片,它包含了所有的命令行参数,第一个参数是程序的名称。

fmt.Println("Hello World", os.Args[1]): 如果有参数传递给程序,就会执行这行代码。它使用 fmt.Println 函数打印一条消息,消息由字符串 “Hello World” 和 os.Args[1] 组成,os.Args[1] 表示传递给程序的第一个参数。

综上所述,这段代码涵盖了以下知识点:

包导入和使用标准库:通过 import 关键字导入 “fmt” 和 “os” 包,然后在代码中使用这些包提供的函数和类型。

命令行参数获取:使用 os.Args 获取命令行参数。

条件语句:使用 if 条件语句来判断是否有命令行参数传递给程序。

字符串操作:使用字符串连接操作将 “Hello World” 与命令行参数拼接在一起。

格式化输出:使用 fmt.Println 函数将消息输出到标准输出。

注意:如果没有传递参数给程序,那么这段代码不会打印任何消息。如果传递了多个参数,代码只会使用第一个参数并忽略其他参数。

在该目录下执行“go run hello.go hzy”,运行结果为“hello world hzy”。

PS D:\Microsoft VS Code\GOproject\src\go_code> go run .\1.go hzy
hello world hzy
PS D:\Microsoft VS Code\GOproject\src\go_code> 

基本程序结构编写学习

变量

前提:chapter2目录下创建variables,学习总结如下:

  • 变量声明: 使用var关键字声明一个变量,例如:var x int
  • 类型推断: 可以使用:=操作符进行变量声明和赋值,Go会根据右侧的值自动推断变量类型,例如:y := 5
  • 变量赋值: 使用赋值操作符=给变量赋值,例如:x = 10
  • 多变量声明: 可以同时声明多个变量,例如:var a, b, c int
  • 变量初始化: 变量可以在声明时进行初始化,例如:var name string = "John"
  • 零值: 未初始化的变量会被赋予零值,数字类型为0,布尔类型为false,字符串类型为空字符串等。
  • 短变量声明: 在函数内部,可以使用短变量声明方式,例如:count := 10

新建fib_test.go,背景:简单实用斐波那契数列进行练习

package variables

import "testing"

func TestFibList(t *testing.T) {
	a := 1
	b := 1
	t.Log(a)
	for i := 0; i < 5; i++ {
		t.Log(" ", b)
		tmp := a
		a = b
		b = tmp + a
	}
}

func TestExchange(t *testing.T) {
	a := 1
	b := 2
	// tmp := a
	// a = b
	// b = tmp
	a, b = b, a
	t.Log(a, b)
}

下面逐个解释代码中涉及的知识点:

package variables: 声明了一个名为 “variables” 的包,这是一个用于测试的包名。

import "testing": 导入了Go语言的测试框架 “testing” 包,用于编写和运行测试函数。

func TestFibList(t *testing.T) { ... }: 定义了一个测试函数 “TestFibList”,该函数用于测试斐波那契数列生成逻辑。这是一个测试函数的标准命名,以 “Test” 开头,接着是被测试的函数名。

在测试函数内部,声明了两个整数变量 ab,并将它们初始化为 1,这是斐波那契数列的前两个数。使用 t.Log(a) 打印变量 a 的值到测试日志中。使用循环来生成斐波那契数列的前 5 个数,每次迭代都会将 b 的值打印到测试日志,并更新 ab 的值以生成下一个数。

func TestExchange(t *testing.T) { ... }: 定义了另一个测试函数 “TestExchange”,该函数用于测试变量交换的逻辑。

在测试函数内部,声明了两个整数变量 ab,并分别将它们初始化为 1 和 2。使用注释的方式展示了一种变量交换的写法(通过中间变量),但实际上被注释掉了。然后使用 a, b = b, a 这一行代码来实现 ab 的交换,这是Go语言中的一种特有的交换方式,不需要额外的中间变量。使用 t.Log(a, b) 打印交换后的变量值到测试日志中。

输出测试结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestFibList$ go_code/chapter/1

=== RUN   TestFibList
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:8: 1
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:10:   1
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:10:   2
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:10:   3
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:10:   5
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:10:   8
--- PASS: TestFibList (0.00s)
PASS
ok      go_code/chapter/1       0.066s
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestExchange$ go_code/chapter/1

=== RUN   TestExchange
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:24: 2 1
--- PASS: TestExchange (0.00s)
PASS
ok      go_code/chapter/1       0.065s

常量

  • 常量声明: 使用const关键字声明一个常量,例如:const pi = 3.14159

  • 常量赋值: 常量的值在声明时必须被赋值,一旦赋值后不可修改。

  • 枚举常量: 可以使用一组常量来模拟枚举。

  • 类型指定: 常量的类型也可以被指定,例如:const speed int = 300000

  • 常量表达式: 常量可使用表达式计算,例如:const secondsInHour = 60 * 60

  • 无类型常量: 常量可以是无类型的,根据上下文自动推断类型。例如,const x = 5会被推断为整数类型。

//枚举常量:
const (
	Tuesday
	Wednesday
)

例子:

package constant

import "testing"

const (
	Monday = 1 + iota
	Tuesday
	Wednesday
)

// 第一个常量块中,使用了 iota 常量生成器来定义了一系列从 1 开始递增的常量。
// 在这个例子中,Monday 被赋值为 1,Tuesday 被赋值为 2,Wednesday 被赋值为 3。
// iota 在常量块中每次被使用时会递增一次,因此后续的常量会依次递增;
//第二个常量块中,使用了 iota 来定义了一系列按位左移的常量。
// 在这个例子中,Readable 被赋值为 1,Writable 被赋值为 2(二进制中的 10),Executable 被赋值为 4(二进制中的 100)。
// 位运算中,左移操作可以将二进制数向左移动指定的位数。

const (
	Readable = 1 << iota
	Writeable
	Executable
)

func TestCostant1(t *testing.T) {
	t.Log(Monday, Tuesday)
}

func TestCostant2(t *testing.T) {
	a := 1
	t.Log(a&Readable == Readable, a&Writeable == Writeable, a&Executable == Executable)
}

package constant: 声明了一个名为 “constant” 的包,这是一个用于测试的包名。

import "testing": 导入了Go语言的测试框架 “testing” 包,用于编写和运行测试函数。

const (...): 定义了两个常量块。

第一个常量块中,使用了 iota 常量生成器来定义了一系列从 1 开始递增的常量。在这个例子中,Monday 被赋值为 1,Tuesday 被赋值为 2,Wednesday 被赋值为 3。iota 在常量块中每次被使用时会递增一次,因此后续的常量会依次递增;第二个常量块中,使用了 iota 来定义了一系列按位左移的常量。在这个例子中,Readable 被赋值为 1,Writable 被赋值为 2(二进制中的 10),Executable 被赋值为 4(二进制中的 100)。位运算中,左移操作可以将二进制数向左移动指定的位数。

func TestConstant1(t *testing.T) { ... }: 定义了一个测试函数 “TestConstant1”,用于测试第一个常量块中定义的常量。

使用 t.Log(Monday, Tuesday) 打印常量 MondayTuesday 的值到测试日志中。

func TestConstant2(t *testing.T) { ... }: 定义了另一个测试函数 “TestConstant2”,用于测试位运算和常量的使用。

在测试函数内部,声明了一个整数变量 a,并将其初始化为 1,即二进制中的 0001。使用位运算和按位与操作来检查变量 a 是否具有 ReadableWritableExecutable 属性。例如,a&Readable == Readable 表达式检查 a 的二进制表示是否含有 Readable 标志位。使用 t.Log() 打印三个表达式的结果到测试日志中。

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestCostant1$ go_code/chapter/2

=== RUN   TestCostant1
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\2\constant_test.go:24: 1 2
--- PASS: TestCostant1 (0.00s)
PASS
ok      go_code/chapter/2       0.076s
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestCostant2$ go_code/chapter/2

=== RUN   TestCostant2
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\2\constant_test.go:29: true false false
--- PASS: TestCostant2 (0.00s)
PASS
ok      go_code/chapter/2       0.068s

数据类型

前提:chapter2目录下创建 type,学习总结如下:

Go语言具有丰富的内置数据类型,这些数据类型用于表示不同类型的值和数据。以下是对Go语言中一些主要数据类型的总结分析:

  • **整数类型(Integer Types):**Go语言提供不同大小的整数类型,如intint8int16int32int64。无符号整数类型有uintuint8uint16uint32uint64。整数类型的大小取决于计算机的架构,例如32位或64位。

  • **浮点数类型(Floating-Point Types):**Go语言提供float32float64两种浮点数类型,分别对应单精度和双精度浮点数。

  • **复数类型(Complex Types):**Go语言提供complex64complex128两种复数类型,分别对应由两个浮点数构成的复数。

  • **布尔类型(Boolean Type):**布尔类型用于表示真(true)和假(false)的值,用于条件判断和逻辑运算。

  • **字符串类型(String Type):**字符串类型表示一系列字符。字符串是不可变的,可以使用双引号"或反引号`来定义。

  • **字符类型(Rune Type):**字符类型rune用于表示Unicode字符,它是int32的别名。通常使用单引号'来表示字符,如'A'

  • **数组类型(Array Types):**数组是具有固定大小的同类型元素集合。声明数组时需要指定元素类型和大小。

  • **切片类型(Slice Types):**切片是对数组的一层封装,是动态长度的可变序列。切片不保存元素,只是引用底层数组的一部分。

  • **映射类型(Map Types):**映射是键值对的无序集合,用于存储和检索数据。键和值可以是任意类型,但键必须是可比较的。

  • **结构体类型(Struct Types):**结构体是一种用户定义的复合数据类型,可以包含不同类型的字段,每个字段有一个名字和类型。

  • **接口类型(Interface Types):**接口是一种抽象类型,用于定义一组方法。类型实现了接口的方法集合即为实现了该接口。

  • **函数类型(Function Types):**函数类型表示函数的签名,包括参数和返回值类型。函数可以作为参数传递和返回。

  • **通道类型(Channel Types):**通道是用于在协程之间进行通信和同步的一种机制。通道有发送和接收操作。

  • **指针类型(Pointer Types):**指针类型表示变量的内存地址。通过指针可以直接访问和修改变量的值。

Go语言的数据类型具有清晰的语法和语义,支持丰富的内置功能。合理选择和使用不同的数据类型可以提高程序的效率和可读性。

类型示例使用:

package main

import (
	"fmt"
)

type Person struct {
	FirstName string
	LastName  string
	Age       int
}

type Shape interface {
	Area() float64
}

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return 3.14 * c.Radius * c.Radius
}

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

func subtract(a, b int) int {
	return a - b
}

type Operation func(int, int) int


//类型使用方法
func main() {
	fmt.Println("整数类型(Integer Types)")
	var x int = 10
	var y int64 = 100

	fmt.Println(x)
	fmt.Println(y)

	fmt.Println("浮点数类型(Floating-Point Types)")
	var a float32 = 3.14
	var b float64 = 3.1415926
	fmt.Println(a)
	fmt.Println(b)

	fmt.Println("布尔类型(Boolean Type)")
	var isTrue bool = true
	var isFalse bool = false

	fmt.Println(isTrue)
	fmt.Println(isFalse)

	fmt.Println("字符串类型(String Type)")
	str1 := "Hello,"
	str2 := "Go!"

	concatenated := str1 + str2
	fmt.Println(concatenated)

	fmt.Println("切片类型(Slice Types)")
	numbers := []int{1, 2, 3, 4, 5}
	fmt.Println(numbers)

	//修改切片
	numbers[0] = 10
	fmt.Println(numbers)

	//切片操作
	subSlice := numbers[1:4]
	fmt.Println(subSlice)

	fmt.Println("映射类型(Map Types)")
	ages := map[string]int{
		"hzy": 19,
	}
	fmt.Println(ages)
	fmt.Println("hzy ages = ", ages["hzy"])

	//添加新的键值对
	ages["ronaldo"] = 41
	fmt.Println(ages)

	fmt.Println("结构体类型(Struct Type)")
	person := Person{
		FirstName: "zhangsan",
		LastName:  "Lis",
		Age:       30,
	}

	fmt.Println(person)
	fmt.Println("Name",person.FirstName, person.LastName)

	fmt.Println("接口类型(Interface Type)")
	var shape Shape
	circle := Circle{Radius: 5}
	shape = circle
	fmt.Println("Circle Area", shape.Area())

	fmt.Println("函数类型(Function type)")
	var op Operation
	op = add
	result := op(10, 5)
	fmt.Println("Addition:", result)

	fmt.Println("通道类型(Channel Type)")
	messages := make(chan string)
	go func() {
		messages <- "Hello Go"
	}()

	msg := <-messages
	fmt.Println(msg)

	fmt.Println("指针类型(Pointer Types)")
	x = 10
	var ptr *int
	ptr = &x
	fmt.Println("Value of x", x)
	fmt.Println("Value stored in pointer:", *ptr)

	*ptr = 20
	fmt.Println("Updated value of x:", x)
}

下面逐个解释代码中涉及的知识点:

type Person struct { ... }: 定义了一个结构体类型 Person,表示一个人的信息,包括 FirstNameLastNameAge 字段。

type Shape interface { ... }: 定义了一个接口类型 Shape,该接口要求实现一个方法 Area() 返回一个 float64 类型

type Circle struct { ... }: 定义了一个结构体类型 Circle,表示一个圆的半径。

func (c Circle) Area() float64 { ... }:为 Circle 类型实现了 Shape 接口的 Area() 方法,用于计算圆的面积。

func add(a, b int) int { ... }: 定义了一个函数 add,用于执行整数相加操作。

func subtract(a, b int) int { ... }: 定义了一个函数 subtract,用于执行整数相减操作。

type Operation func(int, int) int: 定义了一个函数类型 Operation,它接受两个整数参数并返回一个整数结果。

main() { ... }: 程序的入口函数。

  • 定义了多种不同类型的变量,包括整数、浮点数、布尔、字符串、切片、映射、结构体、接口、函数、通道和指针类型。
  • 演示了不同类型变量的初始化、赋值、访问以及基本操作。
  • 使用切片操作提取部分切片。
  • 演示了映射的使用,包括添加新的键值对和访问键值对。
  • 演示了结构体的定义和初始化,并访问结构体字段。
  • 展示了接口的使用,将 Circle 类型赋值给 Shape 类型变量,并调用接口方法。
  • 演示了函数类型的定义和使用,将不同函数赋值给 Operation 类型变量,并进行调用。
  • 使用通道来实现并发通信,通过匿名函数在 goroutine 中发送和接收消息。
  • 演示了指针的使用,包括创建指针变量、通过指针修改变量的值等操作。

输出结果:

PS D:\Microsoft VS Code\GOproject\src\go_code\chapter\numbertype> go run .\number.go
整数类型(Integer Types)
10
100
浮点数类型(Floating-Point Types)
3.14
3.1415926
布尔类型(Boolean Type)
true
false
字符串类型(String Type)
Hello,Go!
切片类型(Slice Types)
[1 2 3 4 5]
[10 2 3 4 5]
[2 3 4]
映射类型(Map Types)
map[hzy:19]
hzy ages =  19
map[hzy:19 ronaldo:41]
结构体类型(Struct Type)
{zhangsan Lis 30}
Name zhangsan Lis
接口类型(Interface Type)
Circle Area 78.5
函数类型(Function type)
Addition: 15
通道类型(Channel Type)
Hello Go
指针类型(Pointer Types)
Value of x 10
Value stored in pointer: 10
Updated value of x: 20

Go语言中类型转换说明
Go语言支持类型转换,但需要注意一些规则和限制。类型转换用于将一个数据类型的值转换为另一个数据类型,以便在不同的上下文中使用。以下是有关Go语言中类型转换的一些重要信息:

基本类型之间的转换: 可以在基本数据类型之间进行转换,但是必须注意类型的兼容性和可能导致的数据丢失。例如,从int到float64的转换是安全的,但从float64到int可能导致小数部分被截断。

显示类型转换: 在Go中,使用强制类型转换来显式指定将一个值转换为另一个类型。语法是:destinationType(expression)。例如:float64(10)。

非兼容类型之间的转换: 对于不兼容的类型,编译器不会自动进行转换。例如,不能直接将一个string类型转换为int类型。

类型别名的转换: 如果有类型别名(Type Alias),在转换时需要注意使用别名的兼容性。

package main

import "fmt"

func main() {
	//显示类型转换
	var x int = 10
	var y float64 = float64(x)
	fmt.Println(y)

	type Celsius float64
	type Fahrenheit float64
	c := Celsius(25)
	f := Fahrenheit(c*9/5 + 32)
	fmt.Println(f)
}

输出结果:

Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\text.go"
10
77

运算符

前提:chapter2目录下创建 operator,学习总结如下:

其实这部分和其他语言都差不多,个人觉得没啥可复习巩固的。Go语言支持多种运算符,用于执行各种算术、逻辑和比较操作。

以下是一些常见的运算符及其在Go中的使用方式和知识点:

常规运算符:

算术运算符(Arithmetic Operators):

  • +:加法
  • -:减法
  • *:乘法
  • /:除法
  • %:取模(取余数)

赋值运算符(Assignment Operators):

  • =:赋值
  • +=:加法赋值
  • -=:减法赋值
  • *=:乘法赋值
  • /=:除法赋值
  • %=:取模赋值

逻辑运算符(Logical Operators):

  • &&:逻辑与(AND)
  • ||:逻辑或(OR)
  • !:逻辑非(NOT)

比较运算符(Comparison Operators):

  • ==:等于
  • !=:不等于
  • <:小于
  • >:大于
  • <=:小于等于
  • >=:大于等于

位运算符(Bitwise Operators):

  • &:按位与(AND)
  • |:按位或(OR)
  • ^:按位异或(XOR)
  • <<:左移
  • >>:右移

其他运算符:

  • &:取地址运算符
  • *:指针运算符
  • ++:自增运算符
  • --:自减运算符

在使用运算符时,需要考虑以下几点:

  • 运算符的操作数必须与运算符的预期类型匹配。
  • 某些运算符具有更高的优先级,需要使用括号来明确优先级。
  • 运算符的操作数可以是变量、常量、表达式等。

全流程测试:

package main

import (
	"fmt"
	"testing"
)

const (
	Readable = 1 << iota
	Writeable
	Executable
)

func TestOperatorBasic(t *testing.T) {
	//算数运算符
	a := 10
	b := 5
	fmt.Println("Sum:", a+b)
	fmt.Println("Difference:", a-b)
	fmt.Println("Product:", a*b)
	fmt.Println("Quotient:", a/b)
	fmt.Println("Remainder", a%b)

	//逻辑运算符
	x := true
	y := false
	fmt.Println("AND:", x && y)
	fmt.Println("OR:", x || y)
	fmt.Println("NOT:", !x)

	//比较运算符
	fmt.Println("Equal:", a == b)
	fmt.Println("Not Equal:", a != b)
	fmt.Println("Greater Than:", a > b)
	fmt.Println("Less Than:", a < b)
	fmt.Println("Greater Than or Equal:", a >= b)
	fmt.Println("Less Than or Equal:", a <= b)
}

func TestCompareArray(t *testing.T) {
	a := [...]int{1, 2, 3, 4}
	b := [...]int{1, 3, 2, 4}
	//	c := [...]int{1, 2, 3, 4, 5}
	d := [...]int{1, 2, 3, 4}
	t.Log(a == b)
	t.Log(a == d)
}

func TestBitClear(t *testing.T) {
	a := 7    // 0111 (所有标志位都设置)
	a = a &^ Readable    // 清除Readable位
	a = a &^ Executable   // 清除Executable位
	t.Log(a&Readable == Readable, a&Writeable == Writeable, a&Executable == Executable)
}
//&^位清除
//输出结果:Final value: 2 (0010)

输出结果:

func TestOperatorBasic(t *testing.T) {

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestOperatorBasic$ go_code/chapter/operator

=== RUN   TestOperatorBasic
Sum: 15
Difference: 5
Product: 50
Quotient: 2
Remainder 0
AND: false
OR: true
NOT: false
Equal: false
Not Equal: true
Greater Than: true
Less Than: false
Greater Than or Equal: true
Less Than or Equal: false
--- PASS: TestOperatorBasic (0.00s)
PASS
ok      go_code/chapter/operator        (cached)

func TestCompareArray(t *testing.T) {

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestCompareArray$ go_code/chapter/operator

=== RUN   TestCompareArray
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\operator\operator_test.go:45: false
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\operator\operator_test.go:46: true
--- PASS: TestCompareArray (0.00s)
PASS
ok      go_code/chapter/operator        (cached)

func TestBitClear(t *testing.T) {

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestBitClear$ go_code/chapter/operator

=== RUN   TestBitClear
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\operator\operator_test.go:53: false true false
--- PASS: TestBitClear (0.00s)
PASS
ok      go_code/chapter/operator        (cached)

const (...): 定义了三个常量 ReadableWritableExecutable,使用位移操作生成不同的值。

func TestOperatorBasic(t *testing.T) { ... }: 定义了一个测试函数 “TestOperatorBasic”,用于测试基本运算符的使用。

算术运算符:展示了加法、减法、乘法、除法和取余运算;逻辑运算符:展示了逻辑与、逻辑或和逻辑非运算;比较运算符:展示了等于、不等于、大于、小于、大于等于和小于等于运算。

func TestCompareArray(t *testing.T) { ... }: 定义了一个测试函数 “TestCompareArray”,用于测试数组的比较。

声明了两个整数数组 ab,以及另一个数组 d,其中数组 a 和数组 d 的内容相同;使用比较运算符 == 检查数组 ab 是否相等,以及数组 ad 是否相等。

func TestBitClear(t *testing.T) { ... }: 定义了一个测试函数 “TestBitClear”,用于测试位清除操作。

声明一个整数变量 a,并将其初始化为 7,即二进制表示 0111;使用位清除操作 &^a 中的 ReadableExecutable 位清除;使用按位与运算 & 检查 a 是否具有 ReadableWritableExecutable 属性。

**按位清除运算符 &^ **

在Go语言中,&^ 是按位清除运算符(Bit Clear Operator)。它用于将某些位置上的位清零,即将指定位置上的位设置为0。&^ 运算符在处理二进制位操作时非常有用。

&^ 运算符执行以下操作:

对于每个位,如果右侧操作数的对应位为 0,则结果位与左侧操作数相同。

对于每个位,如果右侧操作数的对应位为 1,则结果位被强制设置为 0。

这意味着,&^ 运算符用于“清除”左侧操作数的特定位,使其与右侧操作数的相应位不受影响。写个代码验证下:

package main

import "fmt"

func main() {
	a := 0b11001100 //204
	b := 0b10101010 //170

	result := a &^ b //68

    fmt.Println("示例:", a, b, result)
}

计算过程:

  • 第1位(最高位):a=1, b=1 → 0
  • 第2位:a=1, b=0 → 1
  • 第3位:a=0, b=1 → 0
  • 第4位:a=0, b=0 → 0
  • 第5位:a=1, b=1 → 0
  • 第6位:a=1, b=0 → 1
  • 第7位:a=0, b=1 → 0
  • 第8位(最低位):a=0, b=0 → 0

输出结果:

PS D:\Microsoft VS Code\GOproject\src\go_code> go run .\go.go
示例: 204 170 68

条件语句

if语句

if 语句用于基于条件来决定是否执行某段代码。它的基本语法如下:

if condition {
    // 代码块
} else if anotherCondition {
    // 代码块
} else {
    // 代码块
}

switch 语句

switch 语句用于基于表达式的不同值执行不同的代码分支。****与其他语言不同,Go的switch可以自动匹配第一个满足条件的分支,而无需使用break语句。****它的语法如下:

switch expression {
case value1:
    // 代码块
case value2:
    // 代码块
default:
    // 代码块
}

测试代码:

package condition

import (
	"fmt"
	"testing"
)

func TestConditionIf(t *testing.T) {
	age := 18

	if age < 18 {
		fmt.Println("you are a boy")
	} else if age >= 18 && age <= 60 {
		fmt.Println("you are an adult")
	} else {
		fmt.Println("ypu are a senior citizen")
	}
}

func TestConditionSwitch(t *testing.T) {
	dayOfWeek := 7

	switch dayOfWeek {
	case 1:
		fmt.Println("Monday")
	case 2:
		fmt.Println("Tuseday")
	case 3:
		fmt.Println("Wednesday")
	case 4:
		fmt.Println("Thursday")
	case 5:
		fmt.Println("Friday")
	default:
		fmt.Println("Weekend!")
	}
}

func TestSwitchCaseCondition(t *testing.T) {
	for i := 0; i < 5; i++ {
		switch {
		case i%2 == 0:
			t.Logf("%d is Even", i)  //偶数
		case i%2 == 1:
			t.Logf("%d is Odd", i) //奇数
		default:
			t.Logf("%d is unknow", i)
		}
	}
}

下面逐个解释每个测试函数的内容:

func TestConditionIf(t *testing.T) { ... }:测试 if 语句的使用。根据年龄的不同情况,通过 ifelse ifelse 分支判断是否为未成年人、成年人或老年人。

func TestConditionSwitch(t *testing.T) { ... }:测试 switch 语句的使用。根据 dayOfWeek 的值,使用 switch 语句输出对应的星期几。

func TestSwitchMultiCase(t *testing.T) { ... }:测试 switch 语句多个 case 值的情况。使用 switch 语句判断每个数字的奇偶性,并输出相应的信息。

func TestSwitchCaseCondition(t *testing.T) { ... }:测试 switch 语句中的条件表达式。使用 switch 语句通过对数字取余判断数字的奇偶性,并输出相应的信息。

这些测试函数展示了Go语言中条件语句的不同用法,包括基于条件的分支判断和多个 case 值的处理,以及在 switch 语句中使用条件表达式的情况。

输出结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestConditionIf$ go_code/chapter/condition

=== RUN   TestConditionIf
you are an adult
--- PASS: TestConditionIf (0.00s)
PASS
ok      go_code/chapter/condition       0.059s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestConditionSwitch$ go_code/chapter/condition

=== RUN   TestConditionSwitch
Weekend!
--- PASS: TestConditionSwitch (0.00s)
PASS
ok      go_code/chapter/condition       0.057s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestSwitchCaseCondition$ go_code/chapter/condition

=== RUN   TestSwitchCaseCondition
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\condition\condition_test.go:43: 0 is Even
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\condition\condition_test.go:45: 1 is Odd
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\condition\condition_test.go:43: 2 is Even
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\condition\condition_test.go:45: 3 is Odd
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\condition\condition_test.go:43: 4 is Even
--- PASS: TestSwitchCaseCondition (0.00s)
PASS
ok      go_code/chapter/condition       (cached)

## 循环语句

for 循环

for 循环用于重复执行代码块,支持初始化语句、循环条件和循环后的语句。它的基本形式如下:

for initialization; condition; post {
    // 代码块
}

在初始化语句中,您可以初始化循环变量,然后在循环体中使用条件来控制循环,最后在 post 语句中执行递增或递减操作。

for 循环的简化形式

Go语言的 for 循环还可以简化成只有循环条件部分,类似于其他语言中的 while 循环:

for condition {
    // 代码块
}

range 循环

range 循环用于迭代数组、切片、映射、字符串等可迭代的数据结构。它返回每次迭代的索引和值。示例:

for index, value := range iterable {
    // 使用 index 和 value
}

创建loop_test.go进行验证分析, 具体代码如下:

package loop

import (
	"fmt"
	"testing"
)

func TestLoopFor(t *testing.T) {
	for i := 1; i <= 5; i++ {
		fmt.Println("Iteration:", i)
	}
}

func TestLoopForBasic(t *testing.T) {
	i := 1
	for i <= 5 {
		fmt.Println("Iteration:", i)
		i++
	}
}

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

func TestLoopForUnLimit(t *testing.T) {
	i := 1
	for {
		fmt.Println("Iteration:", i)
		i++
		if i > 5 {
			break
		}
	}
}

下面逐个解释每个测试函数的内容:

func TestLoopFor(t *testing.T) { ... }:测试基本的 for 循环。使用 for 循环,从 1 到 5 迭代输出循环迭代次数。

func TestLoopForBasic(t *testing.T) { ... }:测试不带初始化语句的 for 循环。使用 for 循环,从 1 到 5 迭代输出循环迭代次数,但没有在循环头部声明初始化语句。

func TestLoopForRange(t *testing.T) { ... }:测试使用 for range 迭代切片。定义一个整数切片 numbers,使用 for range 循环迭代切片中的每个元素,输出元素的索引和值。

func TestLoopForUnLimit(t *testing.T) { ... }:测试无限循环及 break 语句。使用无限循环和 break 语句,在循环体内部判断是否终止循环,当 i 大于 5 时退出循环。

这些测试函数展示了Go语言中不同类型的 for 循环的用法,包括标准的计数循环、不带初始化语句的循环、遍历切片以及无限循环与循环终止条件。

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestLoopFor$ go_code/chapter/loop

=== RUN   TestLoopFor
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
--- PASS: TestLoopFor (0.00s)
PASS
ok      go_code/chapter/loop    0.073s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestLoopForBasic$ go_code/chapter/loop

=== RUN   TestLoopForBasic
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
--- PASS: TestLoopForBasic (0.00s)
PASS
ok      go_code/chapter/loop    0.060s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestLoopForRange$ go_code/chapter/loop

=== RUN   TestLoopForRange
Index: 0, Value: 1
Index: 1, Value: 2
Index: 2, Value: 3
Index: 3, Value: 4
Index: 4, Value: 5
--- PASS: TestLoopForRange (0.00s)
PASS
ok      go_code/chapter/loop    0.063s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestLoopForUnLimit$ go_code/chapter/loop

=== RUN   TestLoopForUnLimit
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
--- PASS: TestLoopForUnLimit (0.00s)
PASS
ok      go_code/chapter/loop    (cached)

跳转语句

Go语言也支持几种跳转语句,用于在循环和条件中控制流程:

  • break:跳出循环。
  • continue:跳过本次循环迭代,继续下一次迭代。
  • goto:在代码中直接跳转到指定标签处**(不推荐使用)**。

创建jump_test.go进行验证分析, 具体代码如下:

package jump

import (
	"fmt"
	"testing"
)

func TestJumpBreak(t *testing.T) {
	for i := 1; i <= 5; i++ {
		if i == 3 {
			break
		}
		fmt.Println("Iteration:", i)
	}
}

func TestJumpContinue(t *testing.T) {
	for i := 1; i <= 5; i++ {
		if i == 3 {
			continue
		}
		fmt.Println("Iteration:", i)
	}
}

func TestJumpGoto(t *testing.T) {
	i := 1

start:
	fmt.Println("Iteration:", i)
	i++
	if i <= 5 {
		goto start
	}
}

下面逐个解释每个测试函数的内容:

func TestJumpBreak(t *testing.T) { ... }:测试 break 语句的使用。使用 for 循环迭代从 1 到 5,但当迭代变量 i 等于 3 时,使用 break 语句终止循环。

func TestJumpContinue(t *testing.T) { ... }:测试 continue 语句的使用。使用 for 循环迭代从 1 到 5,但当迭代变量 i 等于 3 时,使用 continue 语句跳过该次迭代继续下一次迭代。

func TestJumpGoto(t *testing.T) { ... }:测试 goto 语句的使用。使用 goto 语句实现了一个无限循环,即使用标签 startgoto start 在循环体内部跳转到循环的起始位置。循环的终止条件是当 i 大于 5 时。

这些测试函数展示了Go语言中的循环控制跳转语句,包括用于终止循环的 break、用于跳过当前迭代的 continue,以及用于无限循环的 goto 语句。

测试结果一目了然:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestJumpBreak$ go_code/chapter/jump

=== RUN   TestJumpBreak
Iteration: 1
Iteration: 2
--- PASS: TestJumpBreak (0.00s)
PASS
ok      go_code/chapter/jump    0.061s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestJumpContinue$ go_code/chapter/jump

=== RUN   TestJumpContinue
Iteration: 1
Iteration: 2
Iteration: 4
Iteration: 5
--- PASS: TestJumpContinue (0.00s)
PASS
ok      go_code/chapter/jump    0.062s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestJumpGoto$ go_code/chapter/jump

=== RUN   TestJumpGoto
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
--- PASS: TestJumpGoto (0.00s)
PASS
ok      go_code/chapter/jump    (cached)

常用集合和字符串

数组

Go语言中的数组是一种固定长度、同类型元素的集合。

特点:

  • 数组的长度在声明时指定,且在创建后不可更改。
  • 数组是值类型,当数组被赋值给新变量或作为参数传递时,会创建一个新的副本。
  • 数组在内存中是连续存储的,支持随机访问。

数组声明和初始化:格式为

var arrayName [size]dataType
  • arrayName:数组的名称。
  • size:数组的长度,必须是一个常量表达式。
  • dataType:数组存储的元素类型。

数组初始化方式:

// 使用指定的值初始化数组
var arr = [5]int{1, 2, 3, 4, 5}
 
// 根据索引初始化数组
var arr [5]int
arr[0] = 10
arr[1] = 20
 
// 部分初始化
var arr = [5]int{1, 2}
 
// 自动推断数组长度
arr := [...]int{1, 2, 3, 4, 5}

数组的访问和遍历:

// 访问单个元素
value := arr[index]
 
// 遍历数组
for index, value := range arr {
    fmt.Printf("Index: %d, Value: %d\n", index, value)
}
//输出结果类似于:
Index: 0, Value: 1
Index: 1, Value: 2
Index: 2, Value: 3
Index: 3, Value: 4
Index: 4, Value: 5

数组作为函数:数组在函数参数传递时会创建副本,因此对函数内的数组修改不会影响原始数组。如果需要在函数内修改原始数组,可以传递指向数组的指针。

func modifyArray(arr [5]int) {
    arr[0] = 100
}
 
func modifyArrayByPointer(arr *[5]int) {
    arr[0] = 100
}

多维数组

Go语言支持多维数组,例如二维数组和三维数组。多维数组的初始化和访问与一维数组类似,只需要指定多个索引。

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

**数组在存储固定数量的同类型元素时非常有用,但由于其固定长度的限制,通常在实际开发中更常用的是切片,它具有动态长度的特性。**切片可以根据需要进行增加、删除和重新分配,更加灵活。

测试代码:

package array

import "testing"

func TestArrayInit(t *testing.T) {
	var arr [3]int
	arr1 := [4]int{1, 2, 3, 4}   //[1 2 3 4]
	arr3 := [...]int{1, 3, 4, 5} //[1 3 4 5]
	arr1[1] = 5                  //修改数组arr1的第2个元素(索引从0开始)
	t.Log(arr[1], arr[2])        //所有元素会被自动初始化为int类型的零值(即0)
	t.Log(arr1, arr3)
}

//输出结果
// 0 0
//[1 5 3 4] [1 3 4 5]

func TestArrayTravel(t *testing.T) {
	arr3 := [...]int{1, 3, 4, 5}

	for i := 0; i < len(arr3); i++ {
		t.Log(arr3[i])
	}
	for _, e := range arr3 {
		t.Log(e)
	}
}

func TestArraySection(t *testing.T) {
	arr3 := [...]int{1, 2, 3, 4, 5}
	arr3_sec := arr3[:]

	t.Log(arr3_sec)
}

下面逐个解释每个测试函数的内容:

func TestArrayInit(t *testing.T) { ... }:测试数组的初始化。

使用不同的方式初始化数组 arrarr1arr3;修改 arr1 的第二个元素为 5;使用 t.Log() 输出不同数组的元素值和内容。

func TestArrayTravel(t *testing.T) { ... }:测试数组的遍历。

使用 for 循环遍历数组 arr3,分别输出每个元素的值;使用 for range 循环遍历数组 arr3,同样输出每个元素的值。

func TestArraySection(t *testing.T) { ... }:测试数组切片的使用。

创建一个数组切片 arr3_sec,基于整个数组 arr3;使用 t.Log() 输出数组切片 arr3_sec 的内容。

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestArrayInit$ go_code/chapter/array1

=== RUN   TestArrayInit
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:10: 0 0
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:11: [1 5 3 4] [1 3 4 5]
--- PASS: TestArrayInit (0.00s)
PASS
ok      go_code/chapter/array1  0.074s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestArrayTravel$ go_code/chapter/array1

=== RUN   TestArrayTravel
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:22: 1
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:22: 3
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:22: 4
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:22: 5
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:25: 1
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:25: 3
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:25: 4
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:25: 5
--- PASS: TestArrayTravel (0.00s)
PASS
ok      go_code/chapter/array1  0.064s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestArraySection$ go_code/chapter/array1

=== RUN   TestArraySection
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:33: [1 2 3 4 5]
--- PASS: TestArraySection (0.00s)
PASS
ok      go_code/chapter/array1  (cached)

切片

Go语言中的切片(Slice)是对数组的一层封装,提供了更灵活的动态长度序列。

切片的特点

  • 切片是引用类型,它不保存数据,只是引用底层数组的一部分。
  • 切片是动态长度的,可以根据需要进行扩容或缩减。
  • 切片是可索引的,并且可以通过切片索引进行切割。

切片的声明和初始化

var sliceName []elementType

切片的初始化方式

// 声明切片并初始化
var slice = []int{1, 2, 3, 4, 5}
 
// 使用 make 函数创建切片
var slice = make([]int, 5) // 创建长度为 5 的 int 类型切片
 
// 使用切片切割已有数组或切片
newSlice := oldSlice[startIndex:endIndex] // 包括 startIndex,但不包括 endIndex

切片的内置函数和操作

  • len(slice):返回切片的长度。
  • cap(slice):返回切片的容量,即底层数组的长度。
  • append(slice, element):将元素追加到切片末尾,并返回新的切片。
  • copy(destination, source):将源切片中的元素复制到目标切片。

切片的遍历

for index, value := range slice {
    // 使用 index 和 value
}

切片作为函数参数

切片作为参数传递给函数时,函数内部对切片的修改会影响到原始切片。

func modifySlice(s []int) {
    s[0] = 100
}
 
func main() {
    numbers := []int{1, 2, 3, 4, 5}
    modifySlice(numbers)
    fmt.Println(numbers) // 输出:[100 2 3 4 5]
}

切片在Go语言中广泛用于处理动态数据集,例如集合、列表、队列等。它提供了方便的方法来管理元素,同时避免了固定数组的限制。在实际应用中,切片经常被用于存储和处理变长数据。

创建slice_test.go进行验证分析, 具体代码如下:

package slice

import (
	"fmt"
	"testing"
)

func TestSlice(t *testing.T) {
	//声明和初始化切片
	numbers := []int{1, 2, 3, 4, 5}
	fmt.Println("Original Slice:", numbers)
	//输出:Original Slice: [1 2 3 4 5]

	//使用make函数创建切片
	slice := make([]int, 3)
	fmt.Println("Initial Make Slice:", slice)
	//输出:Initial Make Slice: [0 0 0]

	//添加元素到切片
	slice = append(slice, 10)
	slice = append(slice, 20, 30)
	fmt.Println("After Append:", slice)
	//输出:After Append: [0 0 0 10 20 30]

	//复制切片
	copySlice := make([]int, len(slice))
	copy(copySlice, slice)
	fmt.Println("Copied Slic:e:", copySlice)
	//输出:Copied Slice: [0 0 0 10 20 30]

	//切片切割
	subSlice := numbers[1:3]
	fmt.Println("Subslice:", subSlice)
	//输出:Subslice: [2 3]

	//修改切片值会影响底层数组和其他切片
	subSlice[0] = 100
	fmt.Println("Modified Subslice:", subSlice)
	fmt.Println("Orginal Slice:", numbers)
	fmt.Println("Copied Slice:", copySlice)
	//Copied Slice: [0 0 0 10 20 30]

	//遍历切片
	for index, value := range slice {
		fmt.Printf("Index:%d,Value:%d\n", index, value)
	}
}

下面逐个解释每个测试函数的内容:

func TestSlice(t *testing.T) { ... }:测试切片的基本操作。

  • 声明和初始化切片 numbers,输出初始切片内容。
  • 使用 make 函数创建初始容量为 3 的切片 slice,输出初始切片内容。
  • 使用 append 函数向切片 slice 添加元素。
  • 使用 copy 函数复制切片 slice 到新的切片 copySlice
  • 使用切片 numbers 进行切片切割,创建子切片 subSlice
  • 修改 subSlice 的第一个元素为 100,输出修改后的切片和原始切片,以及复制的切片。
  • 使用 for range 循环遍历切片 slice,输出每个元素的索引和值。

这个测试函数展示了Go语言中切片的各种操作,包括切片的创建、添加元素、复制切片、切片切割、修改切片元素等。

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestSlice$ go_code/chapter/slice

=== RUN   TestSlice
Original Slice: [1 2 3 4 5]
Initial Make Slice: [0 0 0]
After Append: [0 0 0 10 20 30]
Copied Slic:e: [0 0 0 10 20 30]
Subslice: [2 3]
Modified Subslice: [100 3]
Orginal Slice: [1 100 3 4 5]
Copied Slice: [0 0 0 10 20 30]
Index:0,Value:0
Index:1,Value:0
Index:2,Value:0
Index:3,Value:10
Index:4,Value:20
Index:5,Value:30
--- PASS: TestSlice (0.00s)
PASS
ok      go_code/chapter/slice   0.405s

Map

Go语言中的映射(Map)是键值对的无序集合,也被称为关联数组或字典。

映射的特点

  • 映射用于存储一组键值对,其中每个键都是唯一的。
  • 映射是无序的,无法保证键值对的顺序。
  • 键可以是任何可比较的类型,值可以是任意类型。
  • 映射是引用类型,可以被赋值和传递给函数。

映射的声明和初始化

var mapName map[keyType]valueType

映射的初始化方式

// 声明和初始化映射
var ages = map[string]int{
    "Alice": 25,
    "Bob":   30,
    "Eve":   28,
}
 
// 使用 make 函数创建映射
var ages = make(map[string]int)

映射的操作

  • 添加键值对:ages["Charlie"] = 35
  • 删除键值对:delete(ages, "Eve")
  • 获取值:value := ages["Alice"]

映射的遍历

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

映射作为函数参数

映射作为参数传递给函数时,函数内部对映射的修改会影响到原始映射。

func modifyMap(m map[string]int) {
    m["Alice"] = 30
}
 
func main() {
    ages := map[string]int{
        "Alice": 25,
        "Bob":   30,
    }
    modifyMap(ages)
    fmt.Println(ages) // 输出:map[Alice:30 Bob:30]
}

映射在Go语言中用于存储和检索数据,是一种非常常用的数据结构。它在存储一组关联的键值对时非常有用,比如存储姓名与年龄的对应关系、单词与定义的对应关系等。在实际应用中,映射是处理和存储键值数据的重要工具。

创建map_test.go进行验证分析, 具体代码如下:

package my_map

import (
	"fmt"
	"testing"
)

func TestBasic(t *testing.T) {
	//声明和初始化映射
	ages := map[string]int{
		"hzy": 18,
		"thm": 19,
		"txd": 30,
	}
	fmt.Println("Original Map:", ages)

	//添加新的键值对
	ages["Charlie"] = 20
	fmt.Println("After Adding:", ages)

	//修改已有的键值对
	ages["hzy"] = 21
	fmt.Println("After Modification:", ages)

	//删除键值对
	delete(ages, "thm")
	fmt.Println("After Delection:", ages)

	//获取值和检查校验值是否存在
	age, exists := ages["txd"]
	if exists {
		fmt.Println("txd ages is :", age)
	} else {
		fmt.Println("txd not found")
	}

	//遍历映射
	for name, age := range ages {
		fmt.Printf("Name:%sm,Age:%d\n", name, age)
	}
}

type Student struct {
	Name  string
	Age   int
	Grade string
}

//定义结构体,三个字段,结构体用于组织相关联的数据

func TestComplex(t *testing.T) {
	// 声明和初始化映射,用于存储学生信息和成绩
	studentScores := make(map[string]int)
	studentInfo := make(map[string]Student)

	// 添加学生信息和成绩
	studentInfo["Alice"] = Student{Name: "Alice", Age: 18, Grade: "A"}
	studentScores["Alice"] = 80

	// 查找学生信息和成绩
	aliceInfo := studentInfo["Alice"]
	aliceScore := studentScores["Alice"]
	fmt.Printf("Name: %s, Age: %d, Grade: %s, Score: %d\n", aliceInfo.Name, aliceInfo.Age, aliceInfo.Grade, aliceScore)

	// 遍历学生信息和成绩
	for name, info := range studentInfo {
		score, exits := studentScores[name]
		if exits {
			fmt.Printf("Name:%s,Age:%d,Grade:%s,Score:%d\n", info.Name, info.Age, info.Grade, score)
		} else {
			fmt.Printf("No score available for %s\n", name)
		}
	}
}

下面逐个解释每个测试函数的内容:

func TestBasic(t *testing.T) { ... }:测试映射的基本操作。

声明和初始化映射 ages,存储人名和年龄的键值对;输出初始映射内容;使用 ages["Charlie"] 添加新的键值对;使用 ages["Bob"] 修改已有键的值;使用 delete 函数删除键值对;使用 age, exists 来获取值并检查键是否存在;使用 for range 循环遍历映射,输出每个键值对的信息。

type Student struct { ... }:定义了一个名为 Student 的结构体,用于存储学生信息。

func TestComplex(t *testing.T) { ... }:测试包含复杂值的映射操作。

声明和初始化两个映射,studentScores 用于存储学生分数,studentInfo 用于存储学生信息;添加学生信息和分数到映射;使用 studentInfo["Alice"] 获取学生信息,使用 studentScores["Alice"] 获取学生分数;使用 for range 循环遍历映射,输出每个学生的信息和分数。

这些测试函数展示了Go语言中映射的各种操作,包括创建、添加、修改、删除键值对,检查键是否存在,以及遍历映射的键值对。

输出结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestBasic$ go_code/chapter/map

=== RUN   TestBasic
Original Map: map[hzy:18 thm:19 txd:30]
After Adding: map[Charlie:20 hzy:18 thm:19 txd:30]
After Modification: map[Charlie:20 hzy:21 thm:19 txd:30]
After Delection: map[Charlie:20 hzy:21 txd:30]
txd ages is : 30
Name:hzym,Age:21
Name:txdm,Age:30
Name:Charliem,Age:20
--- PASS: TestBasic (0.00s)
PASS
ok      go_code/chapter/map     0.425s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestComplex$ go_code/chapter/map

=== RUN   TestComplex
Name: Alice, Age: 18, Grade: A, Score: 80
Name:Alice,Age:18,Grade:A,Score:80
--- PASS: TestComplex (0.00s)
PASS
ok      go_code/chapter/map     0.073s

实现Set

在Go语言中,虽然标准库没有提供内置的Set类型,但你可以使用多种方式来实现Set的功能。以下是几种常见的实现Set的方式介绍:

使用切片

创建set_slice_test.go练习

使用切片来存储元素,通过遍历切片来检查元素是否存在。这是一个简单的实现方式,适用于小型的集合。

package set

import (
	"fmt"
	"testing"
)

type InSet struct {
	elements []int
}

func (s *InSet) Add(element int) {
	if !s.Contains(element) {
		s.elements = append(s.elements, element)
	}
}

// 先检查元素是否已存在
// 不存在时才添加到切片
// 使用指针接收者(*IntSet)因为要修改结构体

func (s *InSet) Contains(element int) bool {
	for _, e := range s.elements {
		if e == element {
			return true
		}
	}
	return false
}

//线性搜索切片中的元素;找到返回true,否则返回false

func TestSet(t *testing.T) {
	set := InSet{}
	set.Add(1)
	set.Add(2)
	set.Add(3)
	set.Add(2) // 这个重复添加会被忽略

	fmt.Println("Set:", set.elements)
}

测试结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestSet$ go_code/chapter/set_slice

=== RUN   TestSet
Set: [1 2 3]
--- PASS: TestSet (0.00s)
PASS
ok      go_code/chapter/set_slice       0.407s

使用映射

创建set_map_test.go练习

使用映射来存储元素,映射的键代表集合的元素,值可以是任意类型。这样的实现方式更快速,适用于大型的集合,因为映射的查找复杂度为 O(1)

package set

import (
	"fmt"
	"testing"
)

type Set map[int]bool

func (s Set) Add(element int) {
	s[element] = true
}

func (s Set) Contains(element int) bool {
	return s[element]
}

func TestSetMap(t *testing.T) {
	set := make(Set)
	set.Add(1)
	set.Add(2)
	set.Add(3)
	fmt.Println("Set:", set)
}

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestSetMap$ go_code/chapter/set_slice

=== RUN   TestSetMap
Set: map[1:true 2:true 3:true]
--- PASS: TestSetMap (0.00s)
PASS
ok      go_code/chapter/set_slice       0.162s

使用第三方库

创建set_third_test.go练习

为了避免自行实现,你可以使用一些第三方库,例如 github.com/deckarep/golang-set,它提供了更丰富的Set功能。

添加个代理:go env -w GOPROXY=https://goproxy.io,direct

然后安装包:go get github.com/deckarep/golang-set

PS D:\Microsoft VS Code\GOproject\src\go_code\chapter> go get github.com/deckarep/golang-set
go: downloading github.com/deckarep/golang-set v1.8.0
go: added github.com/deckarep/golang-set v1.8.0
PS D:\Microsoft VS Code\GOproject\src\go_code\chapter> 

set_third_test.go:

package set

import (
	"fmt"
	"testing"

	mapset "github.com/deckarep/golang-set"
)

func TestSetThird(t *testing.T) {
	inSet := mapset.NewSet()
	inSet.Add(1)
	inSet.Add(2)
	inSet.Add(3)
	fmt.Println("Set:", inSet)
}

测试结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestSetThird$ go_code/chapter

=== RUN   TestSetThird
Set: Set{1, 2, 3}
--- PASS: TestSetThird (0.00s)
PASS
ok      go_code/chapter 0.177s

字符串

字符串的声明与初始化

在Go语言中,字符串是由一系列字符组成的,可以使用双引号 " 或反引号 ```来声明和初始化字符串

package main

import "fmt"

func main() {
	str1 := "Hello World!"  //双引号
	str2 := `Hello Go!` //反引号

	fmt.Println(str1)
	fmt.Println(str2)
}

输出:

[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
Hello World!
Hello Go!

[Done] exited with code=0 in 1.392 seconds

字符串的长度

使用内置函数 len() 可以获取字符串的长度,即字符串中字符的个数。

package main

import "fmt"

func main() {
	str1 := "Hello World"
	length := len(str1)
	fmt.Println("String Length is ", length)
}

输出:

[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
String Length is  11

[Done] exited with code=0 in 1.404 seconds

字符串的索引与切片

字符串中的字符可以通过索引访问,索引从0开始。可以使用切片操作来获取字符串的子串。

package main

import "fmt"

func main() {
	Str := "Hello,World"

	//获取第一个字符
	firstChar := Str[0]
	fmt.Println("First Character:", string(firstChar))

	//获取子串
	substring := Str[6:11]
	fmt.Println("Substring:", substring)
}

输出:

[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
First Character: H
Substring: World

[Done] exited with code=0 in 1.552 seconds

字符串拼接

使用 + 运算符可以将两个字符串连接成一个新的字符串。

strings.Join 函数用于将字符串切片连接成一个新的字符串,可以用来拼接多个字符串。

最后,使用字节缓冲可以在不产生多余字符串副本的情况下进行高效的字符串拼接。

package main

import (
	"bytes"
	"fmt"
	"strings"
)

func main() {
	str1 := "Hello,"
	str2 := "World!"

	result1 := str1 + str2
	fmt.Println("Concatenated String:", result1)

	//拼接字符串切片(slice)
	strSlice := []string{"Hello", "", "World!"}
	result2 := strings.Join(strSlice, "")
	fmt.Println(result2)

	//使用缓冲区,避免频繁内存分配
	var buffer bytes.Buffer
	buffer.WriteString(str1)
	buffer.WriteString(str2)
	result3 := buffer.String()
	fmt.Println(result3)
}

//总结:
// 少量简单拼接:使用 + 运算符
// 已有字符串切片:使用 strings.Join
// 大量或循环拼接:使用 bytes.Buffer(或 Go 1.10+ 的 strings.Builder)

输出结果:

[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
Concatenated String: Hello,World!
HelloWorld!
Hello,World!

[Done] exited with code=0 in 1.435 seconds

多行字符串

使用反引号 ``` 来创建多行字符串。

package main

import "fmt"

func main() {
	multiLineStr := `
		this is a 
		multi-line
		string.	
	`
	fmt.Println(multiLineStr)
}

输出结果:

[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"

		this is a 
		multi-line
		string.	
	

[Done] exited with code=0 in 1.401 seconds

字符串迭代

使用 for range 循环迭代字符串的每个字符。

package main

import "fmt"

func main() {
	str := "Go 语言"

	for _, char := range str {
		fmt.Printf("%c", char)
	}
}

输出结果:

[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
Go 语言
[Done] exited with code=0 in 1.366 seconds

字符串和字节数组之间的转换

在Go语言中,字符串和字节数组之间可以进行相互转换。

package main

import "fmt"

func main() {
	str := "Hello"
	bytes := []byte(str)   
    // 字符串转字节切片
	//Go 的字符串本质是只读的字节切片([]byte),存储的是字符串的UTF-8 编码字节序列
	// 每个字节对应字符的 ASCII 码:
	// H → 72
	// e → 101
	// l → 108
	// l → 108
	// o → 111
	strAgain := string(bytes)
    //string(bytes) 会基于字节切片创建一个新的字符串,复制字节数据。
	//转换后的字符串仍然是UTF-8 编码
	fmt.Println("Bytes:", bytes)
	fmt.Println("String Again:", strAgain)
}

输出结果:

[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
Bytes: [72 101 108 108 111]
String Again: Hello

[Done] exited with code=0 in 2.307 seconds

字符串比较

字符串的比较可以使用 ==!= 运算符。当然还有其他函数类型的直接应用的:strings.Compare 函数用于比较两个字符串,并根据比较结果返回一个整数。

也可以使用自定义的比较函数来比较字符串,根据自己的需求定义比较逻辑。

package main

import (
	"fmt"
	"strings"
)

func customCompare(str1, str2 string) bool {
	return str1 == str2
}

func main() {
	str1 := "Hello"
	str2 := "World"

	if str1 == str2 {
		fmt.Println("String are equal")
	} else {
		fmt.Println("String are not equal")
	}
	// == 是 Go 语言的基本比较运算符
	// 它会逐个字节比较两个字符串的内容
	// 比较是基于字符串的底层字节表示(UTF-8 编码)

	result := strings.Compare(str1, str2)
	if result == 0 {
		fmt.Println("String are equal")
	} else if result < 0 {
		fmt.Println("str1 is less than str2")
	} else {
		fmt.Println("str1 is greater than str2")
	}
	//返回一个整数:
	// 0 表示 a == b
	// -1 表示 a < b
	// 1 表示 a > b

	if customCompare(str1, str2) {
		fmt.Println("Strings are equal")
	} else {
		fmt.Println("String are not equal")
	}
	// 这实际上是 == 运算符的包装函数
	// 功能与直接使用 == 完全相同
}

输出结果:

[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
String are not equal
str1 is less than str2
String are not equal

[Done] exited with code=0 in 1.22 seconds

这些基本概念和操作可以帮助你更好地理解和使用Go语言中的字符串。要注意字符串的不可变性,以及与其他数据类型的转换和比较。

创建string_test.go练习:

package string

import (
	"strconv"
	"strings"
	"testing"
)

func TestString(t *testing.T) {
	var s string
	t.Log(s) //初始化为默认零值“”空字符串长度为 0,但会占用固定的内存空间
	s = "Hello"
	t.Log(len(s))
	//len(s) 返回字符串的​​字节长度​​,不是字符数
	//s[1] = '3'
	//string是不可变的byte slice
	//s = "\xE4\xB8\xA5" //可以存储任何二进制数据,乱码
	s = "\xE4\xBA\xBB\xFF"
	// Go 字符串可以存储​​任意二进制数据​​,不只是 UTF-8 文本
	// \x 表示十六进制字节值,这里存储了 4 个字节
	// 打印时如果字节序列不是有效的 UTF-8,可能显示为乱码
	// 长度计算的是字节数,不是字符数

	t.Log(s)
	t.Log(len(s))
	s = "中"
	//中文字符"中"在 UTF-8 中占 3 字节
	t.Log(len(s))

	c := []rune(s)
	//将字符串转换为 Unicode 码点序列
	t.Log(len(c))
	// 输出: 1 (1个Unicode字符)
	t.Logf("中 unicode %x", c[0])
	t.Logf("中 UTF8 %x", s)
}

// 字符串与 rune 转换
func TestStringToRune(t *testing.T) {
	s := "中华人民共和国"
	for _, c := range s {
		t.Logf("%[1]c %[1]x", c)
	}
	//每次迭代返回的是 Unicode 码点(rune 类型),而不是字节
	// %[1]c:以字符形式输出 rune
	// %[1]x:以十六进制形式输出 rune 的 Unicode 码点
	// [1] 表示使用第一个参数(即 c)
}

// 字符串分割与连接
func TestStringFn(t *testing.T) {
	s := "A,B,C"
	parts := strings.Split(s, ",")
	for _, part := range parts {
		t.Log(part) //[]string 切片 ["A", "B", "C"]
	}
	t.Log(strings.Join(parts, "-"))
	//strings.Join(parts, "-") 用连字符连接切片元素
	//输出A-B-C
}

// 字符串与数字转换
func TestConv(t *testing.T) {
	s := strconv.Itoa(10) //strconv.Itoa(10) 将整数 10 转换为字符串 "10"
	t.Log("str" + s)      //然后与 "str" 拼接,得到 "str10"
	if i, err := strconv.Atoi("10"); err == nil {
		t.Log(10 + i)
	}
	//strconv.Atoi("10") 将字符串转换为整数,使用 if 语句检查错误,确保转换成功后才进行计算
}

下面逐个解释每个测试函数的内容:

func TestString(t *testing.T) { ... }:测试字符串的基本操作。

声明一个字符串变量 s,输出其默认零值;将字符串赋值为 “hello”,输出字符串长度;尝试修改字符串的某个字符,但会报错,因为字符串是不可变;使用字符串存储二进制数据和 Unicode 编码;使用字符串存储一个中文字符,并输出其长度;将字符串转换为 rune 类型切片,输出切片长度和中文字符的 Unicode 和 UTF-8 编码。

func TestStringToRune(t *testing.T) { ... }:测试字符串到 rune 的转换。

声明一个包含中文字符的字符串 s,通过 range 遍历将字符串转换为 rune 类型并输出。

func TestStringFn(t *testing.T) { ... }:测试字符串相关的函数。

声明一个包含逗号分隔的字符串 s,使用 strings.Split 函数拆分字符串并输出每个部分。使用 strings.Join 函数将拆分的部分合并为一个新的字符串,并输出。

func TestConv(t *testing.T) { ... }:测试字符串与其他类型的转换。

使用 strconv.Itoa 将整数转换为字符串;拼接字符串和整数,并输出结果;使用 strconv.Atoi 将字符串转换为整数,并进行加法运算,处理错误情况。

这些测试函数展示了Go语言中字符串的各种操作,包括字符串长度、UTF-8 编码、rune 类型转换、字符串拆分和合并,以及字符串与其他类型的转换。

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestString$ go_code/chapter/string

=== RUN   TestString
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:11:
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:13: 5
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:24: 亻�
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:25: 4
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:28: 3
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:32: 1
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:34: 中 unicode 4e2d
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:35: 中 UTF8 e4b8ad
--- PASS: TestString (0.00s)
PASS
ok      go_code/chapter/string  0.084s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestStringToRune$ go_code/chapter/string

=== RUN   TestStringToRune
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42:4e2d
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42:534e
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42:4eba
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42:6c11
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42:5171
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42:548c
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42:56fd
--- PASS: TestStringToRune (0.00s)
PASS
ok      go_code/chapter/string  0.065s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestStringFn$ go_code/chapter/string

=== RUN   TestStringFn
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:55: A
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:55: B
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:55: C
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:57: A-B-C
--- PASS: TestStringFn (0.00s)
PASS
ok      go_code/chapter/string  0.064s

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestConv$ go_code/chapter/string

=== RUN   TestConv
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:65: str10
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:67: 20
--- PASS: TestConv (0.00s)
PASS
ok      go_code/chapter/string  0.061s

函数

在Go语言中,函数是一种用于执行特定任务的代码块,可以被多次调用。

print -eg :

fmt.Printffmt.Println 是 Go 语言中两个最常用的格式化输出函数

函数 是否自动换行 示例
Printf 需显式加 \n
Println 自动在末尾添加换行

fmt.Printf

  • 需要格式字符串作为第一个参数
  • 使用占位符指定输出格式(如 %s, %d, %v 等)
%d    十进制整数
%f    浮点数      
%s    字符串
%v    值的默认格式
%+v   显示结构体字段名
%#v   Go语法表示
%T    类型
%%    百分号
  • fmt.Println

    • 自动格式化,不需要占位符
    • 在多个参数间自动添加空格,末尾自动加换行

## 函数基本用法

函数的声明

在Go中,函数的声明由关键字 func 开始,后面跟着函数名、参数列表、返回值和函数体。

func functionName(parameters) returnType {
    // 函数体
    // 可以包含多个语句
    return returnValue
}

函数参数

函数可以有零个或多个参数,参数由参数名和参数类型组成。参数之间使用逗号分隔。

func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

多返回值

Go语言的函数可以返回多个值。返回值用括号括起来,逗号分隔。

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil //nil 表示没有错误
}

命名返回值

函数可以声明命名的返回值,在函数体内可以直接使用这些名称进行赋值,最后不需要显式使用 return 关键字。

func divide(a, b float64) (result float64, err error) {
    if b == 0 {
        err = errors.New("division by zero")
        return
    }
    result = a / b
    return
}

可变数量的参数

Go语言支持使用 ... 语法来表示可变数量的参数。这些参数在函数体内作为切片使用。

func sum(numbers ...int) int {
    total := 0
    for _, num := range numbers {
        total += num
    }
    return total
}

匿名函数和闭包

Go语言支持匿名函数,也称为闭包。这些函数可以在其他函数内部定义,并访问外部函数的变量。

func main() {
    x := 5
    fn := func() {
        fmt.Println(x) // 闭包访问外部变量
    }
    fn() // Output: 5
}

defer语句

defer 语句用于延迟执行函数,通常用于在函数返回前执行一些清理操作。

func main() {
    defer fmt.Println("World")
    fmt.Println("Hello")
}

以上是一些关于Go语言函数的基本知识点。函数在Go中扮演着非常重要的角色,用于组织代码、实现功能模块化和提高代码的可维护性。

基本使用用例验证

创建func_basic_test.go练习:

package basic

import (
	"errors"
	"fmt"
	"testing"
)

// 普通函数
func greet(name string) {
	fmt.Printf("Hello,%s\n", name)
}

// 多返回值函数
func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("division by 0")
	}
	return a / b, nil
}

// 命名返回函数值
func divideNamed(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("division by 0")
	}
	return a / b, nil
}

// 可变数量的参数函数
func sum(numbers ...int) int {
	total := 0
	for _, num := range numbers {
		total += num
	}
	return total
}

// 函数作为参数
func applyFunction(fn func(int, int) int, a, b int) int {
	return fn(a, b)
}

// 匿名函数和闭包
func closureExample() {
	x := 5
	fn := func() {
		fmt.Println(x)
	}
	fn()
}

// defer语句
func deferExample() {
	defer fmt.Println("World")
	fmt.Println("Hello")
	//输出为:Hello World
}

//讲解:
// 遇到 defer 语句时,注册fmt.Println("World") 到 defer 栈
// 执行 fmt.Println("Hello")
// 函数返回前,逆序执行 defer 栈中的调用

func TestBasic(t *testing.T) {
	greet("Hzy")
	//optput:Hello Hzy

	q, err := divide(10, 2)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("Quotient:", q)
	}
	//optput: 5

	qNamed, errNamed := divideNamed(10, 0)
	if errNamed != nil {
		fmt.Println("Error:", errNamed)
	} else {
		fmt.Println("Quotitent:", qNamed)
	}
	//output: Errors:division by 0

	total := sum(1, 2, 3, 4, 5)
	fmt.Println("Sum:", total) //output: Sum:15

	addResult := applyFunction(func(a, b int) int {
		return a + b
	}, 3, 4)
	fmt.Println("Addition:", addResult) //output: Addition:7

	closureExample() //5
	deferExample()
	//Hello
	//World
}

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestBasic$ go_code/chapter/func

=== RUN   TestBasic
Hello,Hzy
Quotient: 5
Error: division by 0
Sum: 15
Addition: 7
5
Hello
World
--- PASS: TestBasic (0.00s)
PASS
ok      go_code/chapter/func    0.395s

## 业务举例

创建func_biz_test.go练习,假设你正在开发一个简单的订单处理系统,需要计算订单中商品的总价和应用折扣。你可以使用函数来处理这些业务逻辑。以下是一个简单的示例:

package biz

import (
	"fmt"
	"testing"
)

type Product struct {
	Name  string
	Price float64
}

// 定义了商品的基本数据结构
// Name 字段表示商品名称(字符串类型)
// Price 字段表示商品价格(浮点数类型)
// 这种结构化的数据表示便于扩展(未来可添加库存、描述等字段)

func calculateTotal(products []Product) float64 {
	total := 0.0
	for _, p := range products {
		total += p.Price
	}
	return total
}

// 接收一个 Product 切片作为输入
// 遍历所有商品,累加价格到 total 变量
// 返回计算出的总价(float64 类型)
//实现购物车商品总价计算的核心逻辑

func applyDiscount(amount, discount float64) float64 {
	return amount * (1 - discount)
}

// 接收原始金额和折扣率(0-1之间的小数)
// 计算折扣后金额:原价 × (1 - 折扣率)
// 返回折扣后金额
// 数学关系明确:如 10% 折扣传入 0.1

func TestBiz(t *testing.T) {
	products := []Product{
		{Name: "Product A", Price: 10.0},
		{Name: "Product B", Price: 20.0},
		{Name: "Product C", Price: 30.0},
	}
	// 商品A:$10
	// 商品B:$20
	// 商品C:$30

	total := calculateTotal(products)
	fmt.Printf("Total before discount: $%2.f\n", total)
	// 计算原始总价:
	// 10 + 20 + 30 = $60

	discountedTotal := applyDiscount(total, 0.1)
	fmt.Printf("Total after 10%% discount : $%.2f\n", discountedTotal)
	// 应用10%折扣:
	// 60 × (1 - 0.1) = $54
}

输出结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestBiz$ go_code/chapter/func

=== RUN   TestBiz
Total before discount: $60
Total after 10% discount : $54.00
--- PASS: TestBiz (0.00s)
PASS
ok      go_code/chapter/func    0.450s

面向对象编程

Go语言支持面向对象编程(Object-Oriented Programming,OOP),尽管与一些传统的面向对象编程语言(如Java和C++)相比,Go的实现方式可能略有不同。在Go语言中,没有类的概念,但可以通过结构体和方法来实现面向对象的特性。

结构体的定义

在Go语言中,结构体是一种自定义的数据类型,用于组合不同类型的字段(成员变量)以创建一个新的数据类型。编写struct_test.go,以下是结构体的定义、使用和验证示例:

package _struct

import (
	"fmt"
	"testing"
)

// 定义一个结构体
type Person struct {
	FirstName string
	LastName  string
	Age       int
}

func TestStruct(t *testing.T) {
	// 创建结构体实例并初始化字段
	personal := Person{
		FirstName: "Alice",
		LastName:  "Smith",
		Age:       10,
	}

	// 访问结构体字段
	fmt.Println("FirstName:", personal.FirstName)
	fmt.Println("LastName:", personal.LastName)
	fmt.Println("Age:", personal.Age)

	// 修改结构体字段的值
	personal.Age = 7
	fmt.Println("Updated age:", personal.Age)
}

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestStruct$ go_code/chapter2/struct

=== RUN   TestStruct
FirstName: Alice
LastName: Smith
Age: 10
Updated age: 7
--- PASS: TestStruct (0.00s)
PASS
ok      go_code/chapter2/struct 0.472s

结构体的定义可以包含多个字段,每个字段可以是不同的数据类型。

你还可以在结构体中嵌套其他结构体,形成更复杂的数据结构。编写struct_cmpx_test.go示例:

package _struct

import (
	"fmt"
	"testing"
)

type Address struct {
	Street  string
	City    string
	ZipCode string
}

type PersonNew struct {
	FirstName string
	LastName  string
	Age       int
	Address   Address
}

func TestCmpxStruct(t *testing.T) {
	person2 := PersonNew{
		FirstName: "Bob",
		LastName:  "Johnson",
		Age:       30,
		Address: Address{
			Street:  "123 Main St",
			City:    "Cityville",
			ZipCode: "12345",
		},
	}

	fmt.Println("Full Name:", person2.FirstName, person2.LastName)
	fmt.Println("Address:", person2.Address.Street, person2.Address.City, person2.Address.ZipCode)
}

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestCmpxStruct$ go_code/chapter/struct/cmpx

=== RUN   TestCmpxStruct
Full Name: Bob Johnson
Address: 123 Main St Cityville 12345
--- PASS: TestCmpxStruct (0.00s)
PASS
ok      go_code/chapter/struct/cmpx     (cached)

实例创建及初始化

在Go语言中,可以通过多种方式来创建和初始化结构体实例。创建creatinit目录,以下是几种常见的实例创建和初始化方法,具体代码为creatinit_test.go

  • 字面量初始化:可以使用花括号 {} 来初始化结构体实例的字段。
  • 部分字段初始化: 如果你只想初始化结构体的部分字段,可以省略其他字段。
  • 使用字段名初始化: 可以根据字段名来指定字段的值,无需按顺序初始化。
  • 默认值初始化: 结构体的字段可以根据其类型的默认值进行初始化。
  • 使用 new 函数: 可以使用 new 函数来创建一个指向结构体的指针,并返回其指针。
  • 字段顺序初始化: 可以选择性地省略字段名,但是这时候需要按照结构体字段的顺序进行赋值。
package creatinit

import (
	"fmt"
	"testing"
)

type Person struct {
	FirstName string
	LastName  string
	Age       int
}

// 字面量初始化
func TestCreate0bj1(t *testing.T) {
	person1 := Person{
		FirstName: "zhangsan",
		LastName:  "Lis",
		Age:       11,
	}
	fmt.Println(person1.FirstName, person1.LastName, person1.Age)
}
//输出:zhangsan Lis 11

func TestCreateObj2(t *testing.T) {
	person2 := Person{
		FirstName: "Bob",
		Age:       30,
	}
	fmt.Println(person2.FirstName, person2.LastName, person2.Age) // Output: Bob  30
}

// 使用字段名初始化
func TestCreate0bj3(t *testing.T) {
	person3 := Person{
		LastName:  "Job",
		FirstName: "Chris",
		Age: 30,
	}
	fmt.Println(person3.FirstName, person3.LastName, person3.Age)
    //output: Chris Job 30
}

// 默认值初始化
func TestCreate0bj4(t *testing.T) {
	var person4 Person
	fmt.Println(person4.FirstName, person4.LastName, person4.Age)
    //output: 0
}

// 使用New函数
func TestCreate0bj5(t *testing.T) {
	person5 := new(Person)
	person5.FirstName = "David"
	person5.Age = 40
	fmt.Println(person5.FirstName, person5.LastName, person5.Age)
    //output:David 40
}

// 字段顺序初始化
func TestCreate0bj6(t *testing.T) {
	person6 := Person{"zhangjie", "xiena", 25}
	fmt.Println(person6.FirstName, person6.LastName, person6.Age)
}
//output: zhangjie xiena 25

行为(方法)定义

在Go语言中,方法是与特定类型相关联的函数,它可以在这个类型的实例上调用。方法使得类型的操作能够与该类型的定义放在一起,提高了代码的可读性和可维护性。

创建method目录进行代码练习,以下是关于Go语言方法的定义、使用和分析:

方法的定义
在Go语言中,方法是通过为函数添加接收者(receiver)来定义的。接收者是一个普通的参数,但它在方法名前放置,用于指定该方法与哪种类型相关联。创建method_define_test.go

package method

import (
	"fmt"
	"testing"
)

// 定义了一个 Circle 结构体,表示圆形
type Circle struct {
	//包含一个字段 Radius 表示半径
	Radius float64
}

// (c Circle):这是方法的接收者,表示这个方法属于 Circle 类型
// Area:方法名称
// float64:返回值类型
func (c Circle) Area() float64 {
	// 方法体计算圆的面积:πr²
	return 3.14159 * c.Radius * c.Radius
}

func TestMethodDef(t *testing.T) {
	//创建 Circle 实例,半径设为 5
	c := Circle{Radius: 5}
	area := c.Area()
	fmt.Printf("Circle area : %.2f\n", area)
}

//eg:
// 方法是与特定类型关联的函数
// 普通函数不属于任何类型

在上述示例中,我们定义了一个 Circle 结构体,然后为其定义了一个名为 Area 的方法。这个方法可以通过 c.Area() 的方式调用,其中 c 是一个 Circle 类型的实例。

输出结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestMethodDef$ go_code/chapter/struct/method

=== RUN   TestMethodDef
Circle area : 78.54
--- PASS: TestMethodDef (0.00s)
PASS
ok      go_code/chapter/struct/method   0.429s

方法的调用

方法调用的语法为 实例.方法名(),即通过实例来调用方法。创建method_rpc_test.go

package method

import (
	"fmt"
	"testing"
)

type Rectangle struct {
	Width  float64
	Height float64
}

func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}

func TestMethodRpc(t *testing.T) {
	rect := Rectangle{Width: 3, Height: 4}
	area := rect.Area()
	fmt.Printf("Rectangle area : %.2f\n", area)
}

指针接收者

Go语言支持使用指针作为方法的接收者,这样可以修改接收者实例的字段值。创建method_rec_test.go

package method

import (
	"fmt"
	"testing"
)

type Counter struct {
	Count int
}

//(c *Counter):指针接收者
// 表示这个方法操作的是原结构体的指针
//方法内部可以直接修改结构体字段的值
func (c *Counter) Increment() {
	c.Count++
}

func TestMethodRec(t *testing.T) {
	counter := Counter{Count: 10}
	counter.Increment()
	fmt.Println("Count:", counter.Count)
}

在上述示例中,Increment 方法使用了指针接收者,这样调用方法后,Count 字段的值会被修改。

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestMethodRec$ go_code/chapter/struct/method

=== RUN   TestMethodRec
Count: 11
--- PASS: TestMethodRec (0.00s)
PASS
ok      go_code/chapter/struct/method      (cached)

方法与函数的区别

方法与函数的主要区别在于方法是特定类型的函数,它与类型的关系更加紧密,可以访问类型的字段和其他方法。函数则是独立于特定类型的代码块。方法通常用于实现特定类型的行为,而函数可以用于通用的操作。

通过定义方法,你可以使类型的操作更加自然和一致,提高代码的可读性和模块化。

这里可以说明一下,method_rpc_test.go中,我们为 Rectangle 结构体定义了一个名为 Area 的方法,该方法可以通过 rect.Area() 的方式调用。方法直接与类型 Rectangle 关联,可以访问 Rectangle 的字段(WidthHeight)。

我们为了与方法作对比,在对应方法体中创建一个方法如下:

// 定义一个函数来计算矩形的面积
func CalculateArea(r Rectangle) float64 {
    return r.Width * r.Height
}

在这个示例中,我们定义了一个名为 CalculateArea 的函数,它接受一个 Rectangle 类型的参数来计算矩形的面积。函数是独立于 Rectangle 类型的,因此它无法直接访问 Rectangle 的字段。

总结: **方法与函数的区别在于方法是特定类型的函数,与类型的关系更加紧密,可以访问类型的字段和其他方法。而函数是独立于特定类型的代码块,通常用于通用的操作。**在上述示例中,方法与矩形相关联,可以直接访问矩形的字段;函数则是一个独立的计算过程,不与任何特定类型直接关联。

通过使用方法,我们可以使代码更加自然和一致,提高代码的可读性和模块化,特别是在实现特定类型的行为时。

接口定义使用

在Go语言中,接口是一种定义方法集合的方式,它规定了一组方法的签名,而不涉及实现细节。通过接口,可以实现多态性和代码解耦,使不同类型的对象能够按照一致的方式进行操作。

定义接口

接口是一组方法的集合,通过 type 关键字定义。接口定义了一组方法签名,但不包含方法的实现。创建interface_test.go进行代码练习

package interfac_test

import (
	"fmt"
	"testing"
)

// 定义一个简单的接口
type Shape interface {
	Area() float64
}
// Shape 是接口名称
// 只包含一个方法签名 Area() float64
// 任何实现了 Area() float64 方法的类型都隐式实现了 Shape 接口
// Go 的接口是隐式实现的,不需要显式声明

// 定义两个实现Shape接口的结构体
// 圆形实现
type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return 3.14159 * c.Radius * c.Radius
}

// 矩形实现
type Rectangle struct {
	Width  float64
	Height float64
}

func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}

func TestInterface(t *testing.T) {
	shapes := []Shape{
		Circle{Radius: 2},
		Rectangle{Width: 3, Height: 4},
	}

	for _, shape := range shapes {
		fmt.Printf("Area of %T: %.2f\n", shape, shape.Area())
	}
}

输出结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestInterface$ go_code/chapter/interface

=== RUN   TestInterface
Area of interfac_test.Circle: 12.57
Area of interfac_test.Rectangle: 12.00
--- PASS: TestInterface (0.00s)
PASS
ok      go_code/chapter/interface     (cached)

在上面的示例中,我们定义了一个名为 Shape 的接口,该接口要求实现一个 Area 方法,用于计算图形的面积。然后,我们定义了两个结构体 CircleRectangle,并分别实现了 Area 方法。通过使用接口,我们可以将不同类型的图形对象放入同一个切片中,然后通过循环调用它们的 Area 方法。

接口的实现

任何类型只要实现了接口中定义的所有方法,就被认为是实现了该接口。接口的实现是隐式的,不需要显式声明。只要方法的签名和接口中的方法签名相同,类型就被视为实现了接口。

接口的多态性

由于接口的多态性,我们可以将实现了接口的对象视为接口本身。在上面的示例中,shapes 切片中存储了不同类型的对象,但它们都实现了 Shape 接口,因此可以通过统一的方式调用 Area 方法。

通过使用接口,可以实现代码的抽象和解耦,使得代码更加灵活和可扩展。接口在Go语言中被广泛应用,用于定义通用的行为和约束。

扩展和复用

在Go语言中,扩展和复用代码的方式与传统的面向对象语言(如Java)有所不同。Go鼓励使用组合、接口和匿名字段等特性来实现代码的扩展和复用,而不是通过类继承。

创建extend目录用于后续练习,以下是关于Go语言中扩展和复用的详细讲解:

组合和嵌套

Go语言中的组合(composition)允许你将一个结构体类型嵌套在另一个结构体类型中,从而实现代码的复用。嵌套的结构体可以通过字段名直接访问其成员。创建composition_test.go

package composition

import (
	"fmt"
	"testing"
)

type Engine struct {
	Model string
}

type Car struct {
	Engine
	Brand  string
	Person string
}

func TestComposition(t *testing.T) {
	car := Car{
		Engine: Engine{Model: "V8s"},
		Brand:  "XiaomiSu7Ultra",
		Person: "雷军",
	}
	fmt.Println("Car Brand:", car.Brand)
	fmt.Println("Car engine model:", car.Model)
	fmt.Println("Car Father:", car.Person)
	//直接访问嵌套结构体字段
}

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestComposition$ go_code/chapter/composition

=== RUN   TestComposition
Car Brand: XiaomiSu7Ultra
Car engine model: V8s
Car Father: 雷军
--- PASS: TestComposition (0.00s)
PASS
ok      go_code/chapter/composition     0.385s

在这个示例中,我们使用了组合来创建 Car 结构体,其中嵌套了 Engine 结构体。通过嵌套,Car 结构体可以直接访问 Engine 结构体的字段。

接口实现

通过接口,可以定义一组方法,然后不同的类型可以实现这些方法。这样可以实现多态性和代码解耦,使得不同类型的对象可以通过相同的接口进行操作。创建interface_ext_test.go

package extend

import (
	"fmt"
	"math"
	"testing"
)

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

// 定义Circle结构体
type Circle struct {
	Radius float64
}

// 实现Circle结构体的方法,以满足Shape接口
func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
	//math.Pi = 3.14....
	//计算面积
}

func (c Circle) Perimeter() float64 {
	return 2 * math.Pi * c.Radius
	//2pair-->计算周长
}

// 定义Rectangle结构体
type Rectangle struct {
	Width  float64
	Height float64
}

// 实现Rectangle结构体的方法,以满足Shape接口
func (r Rectangle) Area() float64 {
	return r.Height * r.Width
	//计算面积
}

func (r Rectangle) Perimeter() float64 {
	return 2 * (r.Height + r.Width)
	//计算周长
}

// 测试函数
// 多态处理:统一接口操作
func TestInterfaceExt(t *testing.T) {
	circle := Circle{Radius: 3}
	rectangle := Rectangle{Width: 4, Height: 5}

	shapes := []Shape{circle, rectangle}

	for _, shape := range shapes {
		fmt.Printf("Share Type: %T\n", shape)
		fmt.Printf("Area: %2.f\n", shape.Area())
		fmt.Printf("Perimeter: %.2f\n", shape.Perimeter())
		fmt.Println("------------------")
	}
}

//深层理解:
// 接口不是用来传递数据到结构体,而是定义一组行为规范。
// 它更像是一份"能力证书",规定了实现者必须具备哪些能力(方法)

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestInterfaceExt$ go_code/chapter/interface/extend

=== RUN   TestInterfaceExt
Shape Type: extend.Circle
Area: 28.27
Perimeter: 18.85
------------
Shape Type: extend.Rectangle
Area: 20.00
Perimeter: 18.00
------------
--- PASS: TestInterfaceExt (0.00s)
PASS
ok      go_code/chapter/interface/extend        0.388s

在上述示例中,我们定义了一个名为 Shape 的接口,它有两个方法 Area() 和 Perimeter(),分别用于计算形状的面积和周长。然后,我们分别实现了 Circle 和 Rectangle 结构体的这两个方法,使它们满足了 Shape 接口。

通过将不同类型的形状实例放入一个 []Shape 切片中,我们可以使用统一的方式调用 Area() 和 Perimeter() 方法,实现了代码的多态性和解耦。这样,无论我们后续添加新的形状,只要它们实现了 Shape 接口的方法,就可以无缝地集成到计算器中。

匿名字段和方法重用

通过使用匿名字段,一个结构体可以继承另一个结构体的字段和方法。创建other_test.go

package extend

import (
	"fmt"
	"testing"
)

type Animal struct {
	Name string
}

func (a Animal) Speak() {
	fmt.Println("Animal speaks")
}

type Dog struct {
	Animal
	Breed string
}

func TestOtherExt(t *testing.T) {
	dog := Dog{
		Animal: Animal{Name: "liuzi"},
		Breed:  "Folden Retriever",
	}

	fmt.Println("Dog name", dog.Name)
	dog.Speak() //继承了Animal的Speak的用法
	//输出:Animal speaks
}

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestOtherExt$ go_code/chapter/interface/extend

=== RUN   TestOtherExt
Dog name liuzi
Animal speaks
--- PASS: TestOtherExt (0.00s)
PASS
ok      go_code/chapter/interface/extend   0.399s

在上述示例中,Dog 结构体嵌套了 Animal 结构体,从而继承了 Animal 的字段和方法。

通过这些方式,你可以在Go语言中实现代码的扩展和复用。尽管Go不像传统的面向对象语言那样强调类继承,但通过组合、接口和匿名字段等特性,你仍然可以实现类似的效果,使代码更灵活、可读性更高,并保持低耦合性。

空接口和断言

空接口和断言是Go语言中用于处理不确定类型和类型转换的重要概念。

创建emptyassert目录用于后续练习,下面是关于空接口和断言的学习总结:

空接口(Empty Interface)

空接口是Go语言中最基础的接口,它不包含任何方法声明。因此,空接口可以用来表示任何类型的值。空接口的声明方式为 interface{}

空接口的主要用途是在需要处理不确定类型的场景中。通过使用空接口,可以接受和存储任何类型的值,类似于其他编程语言中的动态类型。但需要注意的是,使用空接口可能会导致类型安全性降低,因为编译时无法检查具体类型。

断言(Type Assertion)

断言是一种在空接口中恢复具体类型的机制,它允许我们在运行时检查空接口中的值的实际类型,并将其转换为相应的类型。断言的语法为 value.(Type),其中 value 是接口值,Type 是要断言的具体类型。

创建emptyassert_test.go进行验证:

package emptyassert

import (
	"fmt"
	"testing"
)

func DoSomething(p interface{}) {
	switch v := p.(type) {
	//通过类型断言检查 p 的具体类型,v 是转换后的具体类型值。
	case int:
		fmt.Println("Integer", v)
	case string:
		fmt.Println("String", v)
	default:
		fmt.Println("Unknow Type")
	}
}

func TestEmptyInterfaceAssertion(t *testing.T) {
	//传参检测
	DoSomething(10)
	DoSomething("10")
}

func TestEmptyAssert(t *testing.T) {
	var x interface{} = "hello"
	//安全类型断言,检查 x 是否为 string 类型
	str, ok := x.(string)
	if ok {
		fmt.Println("String", str)
	} else {
		fmt.Println("Not a string")
	}
}

// 总结:
// 空接口:interface{} 可存储任意类型的值,但需类型断言或类型切换来访问具体值。
// 类型切换(Type Switch):简化多类型判断,自动提取具体类型的值。
// 安全类型断言:使用 value, ok := x.(T) 避免断言失败时的 panic。
// 测试覆盖:现有测试用例覆盖了 int 和 string,但未测试 default 分支(如传入 float64 会触发)。

下面逐个解释每个测试函数的内容:

func DoSomething(p interface{}) { ... }:定义了一个函数 DoSomething,该函数接受一个空接口参数 p,然后根据接口值的实际类型进行类型断言,根据不同的类型输出不同的信息。

func TestEmptyInterfaceAssertion(t *testing.T) { ... }:测试空接口的断言操作。

调用 DoSomething(10),将整数 10 传递给函数,函数根据类型断言输出整数类型信息。调用 DoSomething("10"),将字符串 "10" 传递给函数,函数根据类型断言输出字符串类型信息。

func TestEmptyAssert(t *testing.T) { ... }:测试空接口的类型断言操作。

声明一个空接口变量 x,并将字符串 "hello" 赋值给它。使用类型断言 x.(string) 判断 x 是否为字符串类型,如果是,将其赋值给变量 str,并输出字符串值;否则输出 “Not a string”。

这些测试函数展示了Go语言中空接口的断言操作,通过类型断言可以判断空接口中的具体类型,并执行相应的操作。

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestEmptyInterfaceAssertion$ go_code/chapter/interface/emptyassert

=== RUN   TestEmptyInterfaceAssertion
Integer 10
String 10
--- PASS: TestEmptyInterfaceAssertion (0.00s)
PASS
ok      go_code/chapter/interface/emptyassert   (cached)

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestEmptyAssert$ go_code/chapter/interface/emptyassert

=== RUN   TestEmptyAssert
String hello
--- PASS: TestEmptyAssert (0.00s)
PASS
ok      go_code/chapter/interface/emptyassert   0.156s

总结: 空接口和断言是Go语言中处理不确定类型和类型转换的强大工具。空接口允许存储任何类型的值,而断言允许我们在运行时检查和转换接口值的实际类型。使用这些机制,可以在需要处理不同类型的值时实现更灵活和通用的代码。但在使用空接口和断言时,要注意维护类型安全性,并进行适当的错误处理。

GO 接口最佳实践

在Go语言中,使用接口的最佳实践可以提高代码的可读性、可维护性和灵活性。

  • 小接口与大接口: 尽量设计小接口,一个接口应该只包含少量的方法,而不是设计一个大而全的接口。这样可以避免实现接口时不必要的负担,并使接口更具通用性。
  • 基于使用场景设计接口: 设计接口时应该考虑使用场景,而不是从具体的实现出发。思考在你的应用程序中如何使用接口,以及接口应该提供哪些方法来满足这些使用场景。
  • 使用合适的命名: 为接口和方法使用清晰的命名,使其能够表达出其用途和功能。命名应该具有可读性和表达性,让其他开发者能够轻松理解接口的用途。
  • 避免不必要的接口: 不要为每个类型都创建一个接口,只有在多个类型之间确实存在共享的行为和功能时才使用接口。不要过度使用接口,以免导致不必要的复杂性。
  • 使用接口作为函数参数和返回值: 使用接口作为函数参数和返回值,可以使函数更加通用,允许传入不同类型的参数,并返回不同类型的结果。这可以提高代码的复用性和扩展性。
  • 注释和文档: 为接口提供清晰的文档和注释,说明接口的用途、方法的功能和预期行为。这可以帮助其他开发者更好地理解接口的使用方式。
  • 用例驱动设计: 在设计接口时,可以从使用的角度出发,先考虑接口在实际场景中如何被调用,然后再设计接口的方法和签名。
  • 将接口的实现与定义分离: 将接口的实现与接口的定义分开,这样可以使实现更灵活,可以在不修改接口定义的情况下实现新的类型。
  • 默认实现: 在接口定义中,可以为某些方法提供默认实现,从而减少实现接口时的工作量。这对于可选方法或者某些方法的默认行为很有用。
  • 使用空接口谨慎: 使用空接口(interface{})应谨慎,因为它会降低类型安全性。只有在确实需要处理不同类型的值时才使用空接口,同时要注意类型断言和错误处理。

设计和使用接口时要根据实际需求和项目的特点来选择合适的方案。

错误类型

基本使用介绍

错误类型

在Go中,错误被表示为一个实现了 error 接口的类型。error 接口只有一个方法,即 Error() string,它返回一个描述错误的字符串。

type error interface {
    Error() string
}

返回错误值

当一个函数遇到错误情况时,通常会返回一个错误值。这个错误值可以是一个实现了 error 接口的自定义类型,也可以是Go标准库中预定义的错误类型,如 errors.New() 创建的错误。

错误检查

调用者通常需要显式地检查函数返回的错误,以判断是否发生了错误。这可以通过在调用函数后使用 if 语句来实现。

以上两个直接写代码如下:

package basic

import (
	"errors"
	"fmt"
	"testing"
)

// 以 Err 开头,定义两种错误类型,用于参数校验。
var ErrLessThanTwoError = errors.New("n should be not less than 2")
var ErrLargerThenHundredError = errors.New("n should be not larger than 100")

// 斐波那契生成模块
func GetFibonacci(n int) ([]int, error) {
	//参数校验
	if n < 2 {
		return nil, ErrLessThanTwoError
	}
	if n > 100 {
		return nil, ErrLargerThenHundredError
	}

	//生成数列,生成前 n 个斐波那契数。
	fibList := []int{1, 1}

	// 若 n < 2 或 n > 100 返回对应错误。
	for i := 2; i < n; i++ {
		fibList = append(fibList, fibList[i-2]+fibList[i-1])
	}
	return fibList, nil

	// 初始化数列为 [1, 1]。
	// 从第3个元素开始(索引2),每个元素是前两个元素之和。
	// 例如:n=5 时,生成 [1, 1, 2, 3, 5]。
}

func TestGetFibonacci(t *testing.T) {
	// 测试 n=1(触发 ErrLessThanTwo)
	if v, err := GetFibonacci(1); err != nil {
		if err == ErrLessThanTwoError {
			fmt.Println("It is less.")
		}
		t.Error(err)
	} else {
		t.Log(v)
	}

}

测试,当n=1时:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestGetFibonacci$ go_code/chapter/basic

=== RUN   TestGetFibonacci
It is less.
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\basic\error_test.go:43: n should be not less than 2
--- FAIL: TestGetFibonacci (0.00s)
FAIL
FAIL    go_code/chapter/basic   0.170s

测试,当n=5时:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestGetFibonacci$ go_code/chapter/basic

=== RUN   TestGetFibonacci
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\basic\error_test.go:45: [1 1 2 3 5]
--- PASS: TestGetFibonacci (0.00s)
PASS
ok      go_code/chapter/basic   (cached)

测试,当n=101时:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestGetFibonacci$ go_code/chapter/basic

=== RUN   TestGetFibonacci
    d:\Microsoft VS Code\GOproject\src\go_code\chapter\basic\error_test.go:43: n should be not larger than 100
--- FAIL: TestGetFibonacci (0.00s)
FAIL
FAIL    go_code/chapter/basic   0.174s

错误链

在某些情况下,错误可以包含附加信息,以便更好地理解错误的原因。可以通过 fmt.Errorf() 函数来创建包含附加信息的错误。

假设我们正在构建一个文件操作的库,其中包含文件读取和写入功能。有时,在文件读取或写入过程中可能会出现各种错误,例如文件不存在、权限问题等。我们希望能够提供有关错误的更多上下文信息。

package chain

import (
	"errors"
	"fmt"
	"testing"
)

type FileError struct {
	Op   string //操作类型("read"/"write")
	Path string //文件路径错误
	Err  error  //原始错误
}

// 实现error接口的Error()方法
func (e *FileError) Error() string {
	return fmt.Sprintf("%s %s:%v", e.Op, e.Path, e.Err)
}

// 结构体包含三个字段:Op(操作类型)、Path(文件路径)、Err(原始错误)。
// 通过实现 Error() string 方法,FileError 满足 error 接口,可直接作为错误返回。
// 错误信息格式示例:read /path/to/file.txt:file not found。

// 模拟文件读取操作
func ReadFile(path string) ([]byte, error) {
	// 模拟文件不存在的情况
	return nil, &FileError{Op: "read", Path: path, Err: errors.New("file not found")}
}

//函数返回一个自定义的 FileError,包装了底层错误 file not found
//通过 &FileError{...} 创建指针类型,确保错误类型可被正确识别

func TestChain(t *testing.T) {
	filePath := "/path/to/file.txt"
	_, err := ReadFile(filePath)

	if err != nil {
		fmt.Println("Error:", err)

		// 在这里,我们可以检查错误类型,提取上下文信息
		// 类型断言解包错误
		if fileErr, ok := err.(*FileError); ok {
			fmt.Printf("Operation:%s\n", fileErr.Op)
			fmt.Printf("File Path:%s\n", fileErr.Path)
			fmt.Printf("Original Error:%v\n", fileErr.Err)
		}
	}
}

// 流程:
// 调用 ReadFile 触发错误。
// 打印完整错误信息(Error: read /path/to/file.txt:file not found)。
// 通过类型断言(err.(*FileError))提取自定义错误中的详细信息。
// 输出操作类型、文件路径和原始错误。

下面是代码的解释:

FileError 结构体:定义了一个自定义错误类型 FileError,包含以下字段:

Op:操作类型,表示是读取(“read”)还是写入(“write”)操作;Path:文件路径,表示涉及哪个文件;Err:原始错误,包含底层的错误信息。

Error() 方法:为 FileError 结构体实现了 error 接口的 Error() 方法,用于生成错误的文本描述。

ReadFile() 函数:模拟文件读取操作。在这个示例中,该函数返回一个 FileError 类型的错误,模拟了文件不存在的情况。

TestChain() 测试函数:演示如何在错误处理中使用自定义错误类型。

定义了一个文件路径 filePath,并调用 ReadFile(filePath) 函数来模拟文件读取操作;检查错误,如果发生错误,输出错误信息;在错误处理中,通过类型断言检查错误是否为 *FileError 类型,如果是,则可以提取更多上下文信息,如操作类型、文件路径和原始错误信息。

个人理解:

“当执行文件读取操作后,若检测到文件路径不存在,函数会 返回 nil 作为数据占位符(表示无有效内容),并通过返回一个指向自定义错误 FileError 的指针来传递错误详情。调用方通过检查 err != nil 来判断是否发生错误,并通过类型断言提取错误上下文(如操作类型、文件路径等)。”

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestChain$ go_code/chapter/error/chain

=== RUN   TestChain
Error: read /path/to/file.txt:file not found
Operation:read
File Path:/path/to/file.txt
Original Error:file not found
--- PASS: TestChain (0.00s)
PASS
ok      go_code/chapter/error/chain     0.449s

Panic 和 Recover

在Go语言中,panicrecover 是用于处理异常情况的机制,但它们应该谨慎使用,仅用于特定的情况,而不是替代正常的错误处理机制。以下是对 panicrecover 的详细解释,并给出一个具体用例:

panic

创建panic目录,编写panic_test.go。

panic 是一个内置函数,用于引发运行时恐慌。当程序遇到无法继续执行的致命错误时,可以使用 panic 来中断程序的正常流程。但应该避免滥用 panic,因为它会导致程序崩溃,不会提供友好的错误信息。典型情况下,panic 用于表示程序中的不可恢复错误,例如切片索引越界。

package panic

import (
	"fmt"
	"testing"
)

// 通过主动触发 panic 避免程序因未处理的越界访问而崩溃。
func TestPanic(t *testing.T) {
	arr := []int{1, 2, 3} // 定义一个切片,长度3,,有效索引为0-2
	index := 4            //越界索引

	// 主动检查索引是否越界
	if index >= len(arr) {
		panic("Index out of range") // 触发自定义 panic
	}
	element := arr[index]
	fmt.Println("Element:", element) // 若未触发 panic,尝试访问元素
}

在上述示例中,如果索引 index 超出了切片 arr 的范围,会触发 panic,导致程序崩溃。这种情况下,panic 用于表示程序的不可恢复错误。

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestPanic$ go_code/chapter/error/panic

=== RUN   TestPanic
--- FAIL: TestPanic (0.00s)
panic: Index out of range [recovered]
        panic: Index out of range

goroutine 19 [running]:
testing.tRunner.func1.2({0x2f6880, 0x3594a0})
        C:/Go/src/testing/testing.go:1632 +0x225
testing.tRunner.func1()
        C:/Go/src/testing/testing.go:1635 +0x359
panic({0x2f6880?, 0x3594a0?})
        C:/Go/src/runtime/panic.go:785 +0x132
go_code/chapter/error/panic.TestPanic(0xc0000d84e0?)
        d:/Microsoft VS Code/GOproject/src/go_code/chapter/error/panic/panic_test.go:15 +0x25
testing.tRunner(0xc0000d84e0, 0x32f7b0)
        C:/Go/src/testing/testing.go:1690 +0xcb
created by testing.(*T).Run in goroutine 1
        C:/Go/src/testing/testing.go:1743 +0x377
FAIL    go_code/chapter/error/panic     0.415s

当index在正确索引时:

//当index=2
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestPanic$ go_code/chapter/error/panic

=== RUN   TestPanic
Element: 3
--- PASS: TestPanic (0.00s)
PASS
ok      go_code/chapter/error/panic     (cached)

recover

创建recover目录,编写recover_test.go。recover 也是一个内置函数,用于恢复 panic 引发的运行时恐慌。它只能在延迟函数(defer)内部使用,并且用于恢复程序的控制流,而不是用于处理错误。通常,在发生 panic 后,recover 可以在延迟函数中捕获 panic,并执行一些清理工作,然后程序会继续执行。//So,这是联用的啊。

package recover

import (
	"fmt"
	"testing"
)

// 清理函数:捕获 panic 并恢复
func cleanup() {
	if r := recover(); r != nil { // 捕获 panic 的值
		fmt.Println("Recoverd from panic:", r) // 处理错误(注意拼写应为 "Recovered")
	}
}

// 测试函数:触发 panic 并恢复
func TestRecover(t *testing.T) {
	defer cleanup()                               // 注册延迟执行的清理函数
	panic("Something went wrong")                 // 主动触发 panic
	fmt.Println("This line will not be executed") // 此代码不会执行
}

在上述示例中,panic 触发后,cleanup 函数中的 recover 捕获了 panic,并打印了错误消息。然后程序会继续执行,但需要注意的是,控制流不会回到触发 panic 的地方,因此 fmt.Println 不会被执行。

总之,panicrecover 应该谨慎使用,只用于特殊情况,如不可恢复的错误或在延迟函数中进行清理操作。在大多数情况下,应该优先使用错误返回值来处理异常情况,因为这种方式更安全、可控,能够提供更好的错误信息和错误处理。只有在特定的情况下,例如遇到不可恢复的错误时,才应该考虑使用 panicrecover

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestRecover$ go_code/chapter/error/recover

=== RUN   TestRecover
Recoverd from panic: Something went wrong
--- PASS: TestRecover (0.00s)
PASS
ok      go_code/chapter/error/recover   0.402s

自定义错误类型

在Go中,你可以根据需要定义自己的错误类型,只需满足 error 接口的要求即可。这允许你创建更具描述性和上下文的错误类型。

在Go中,自定义错误类型是一种强大的方式,可以创建更具描述性和上下文的错误,以提供更好的错误信息。自定义错误类型必须满足 error 接口的要求,即实现 Error() string 方法。以下是一个示例,展示如何自定义错误类型和验证其用例:

package define

import (
	"fmt"
	"testing"
	"time"
)

// 自定义错误类型
type TimeoutError struct {
	Operation string
	Timeout   time.Time
}

// 实现error接口的Error()方法
func (e TimeoutError) Error() string {
	return fmt.Sprintf("Timeout error during %s operation.Timeout at %s",
		e.Operation,
		e.Timeout.Format("2025-04-24 15:00:00")) // Go 固定时间格式模板
}

//封装超时错误的上下文信息
// Timeout 记录具体的超时时间点。
// 实现 Error() string 方法,满足 error 接口,提供可读的错误信息。

// 模拟执行某个操作,可能会超时
func PerformOperation() error {
	timeout := time.Now().Add(-5 * time.Second) // 设置超时时间为当前时间 -5 秒,模拟超时
	if time.Now().After(timeout) {              // 判断当前时间是否已超过超时时间点
		return TimeoutError{Operation: "PerformOperation", Timeout: timeout}
		//模拟成功
	}
	return nil
}

func TestDefineError(t *testing.T) {
	err := PerformOperation()
	if err != nil {
		// 检查错误类型并打印错误信息
		if timeoutError, ok := err.(TimeoutError); ok {
			fmt.Println("Error type:", timeoutError.Operation)
			fmt.Println("Timeout at:", timeoutError.Timeout)
		}
		fmt.Println("Error:", err)
	} else {
		fmt.Println("Operation completed successfully.")
	}
}

// 调用 PerformOperation,预期返回错误
// 错误处理:
// 使用类型断言 err.(TimeoutError) 检查错误类型。
// 若为 TimeoutError,提取并打印操作名称和超时时间。
// 最后打印完整错误信息(通过 Error() 方法)。
// 无错误时:打印成功信息。

输出结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestDefineError$ go_code/chapter/error/define

=== RUN   TestDefineError
Error type: PerformOperation
Timeout at: 2025-04-24 13:19:26.2072294 +0800 CST m=-4.998300499
Error: Timeout error during PerformOperation operation.Timeout at 242426-19-2419 13:00:00
--- PASS: TestDefineError (0.02s)
PASS
ok      go_code/chapter/error/define    0.433s

下面是代码的解释:

TimeoutError 结构体:定义了一个自定义错误类型 TimeoutError,包含以下字段:

Operation:操作名称,表示哪个操作超时;Timeout:超时时间,表示操作发生超时的时间点。

Error() 方法:为 TimeoutError 结构体实现了 error 接口的 Error() 方法,用于生成错误的文本描述。

PerformOperation() 函数:模拟执行某个操作,可能会超时。在这个示例中,如果当前时间超过了超时时间,则返回一个 TimeoutError 类型的错误。

TestDefineError() 测试函数:演示如何在错误处理中使用自定义错误类型。

调用 PerformOperation() 函数来模拟操作,并检查是否发生了错误;如果发生错误,首先检查错误类型是否为 TimeoutError,如果是,则提取超时操作和超时时间,并输出相关信息;最后,无论是否发生错误,都会输出错误信息或成功完成的消息。

这个示例展示了如何自定义错误类型以及如何在错误处理中利用这些自定义错误类型来提供更多的上下文信息,使错误处理更加有信息和灵活。在这里,TimeoutError 提供了有关超时操作和超时时间的额外信息。

包和依赖管理

src目录下创建chapter7,Go 语言的包和依赖管理主要通过其内置的模块系统(Go Modules)来实现。Go Modules 于 Go 1.11 版本首次引入,并在 Go 1.13 版本中成为默认的依赖管理方式。

package(包)的基本知识点

基本复用模块单元

在 Go 语言中,package 是代码复用的基本单元。一个 package 可以包含多个 Go 源文件,这些文件可以共享同一个包中的代码,并通过包的导入机制被其他包使用。

包的可见性在 Go 语言中,通过首字母大写来表明一个标识符(如变量、函数、类型等)可以被包外的代码访问。反之,首字母小写的标识符只能在包内使用。

// mypackage.go
package mypackage
 
// 公有函数,其他包可以访问
func PublicFunction() {
    // 实现细节
}
 
// 私有函数,仅在当前包内可访问
func privateFunction() {
    // 实现细节
}

代码的 package 可以和所在的目录不一致

Go 语言的文件组织结构鼓励但不强制 package 名称与其所在目录名称一致。通常情况下,开发者会遵循这种约定以保持代码的一致性和可读性,但 Go 并不强制执行这一规则。

实际应用:你可以在chapter2目录下创建多个文件,并在这些文件中定义相同的包名 mypackage,也可以选择一个不同于目录名的包名。

// chapter2/1.go
package mypackage


// chapter2/2.go
package mypackage

同一目录里的 Go 代码的 package 要保持一致

在同一目录中的所有 Go 文件必须声明相同的 package 名称。这是 Go 语言的一个基本规则,确保同一目录下的所有文件都属于同一个包,从而能够互相访问这些文件中声明的标识符。

违例情况:如果你在同一目录下使用不同的 package 名称,Go 编译器将会报错,提示包声明不一致。

构建一个自身可复用的package

src目录下创建chapter2后,再次新建series,编写my_series.go如下:

package series

import "fmt"

//当其他代码通过 import "series" 导入该包时
//会依次打印,用于初始化
func init() {
	fmt.Println("init1")
}

func init() {
	fmt.Println("init2")
}

//接收一个整数 n,返回其平方值。
func Square(n int) int {
	return n * n
}

//生成斐波那契数列
func GetFibonacciSerie(n int) []int {
	//处理 n < 2 的情况
	if n <= 0 {
		return []int{}
	}
	ret := []int{1, 1}
	if n <= 2 {
		return ret[:n]
	}
	//生成包含前 n 项的斐波那契数列。
	for i := 2; i < n; i++ {
		ret = append(ret, ret[i-2]+ret[i-1])
	}
	return ret
}

然后在chapter2中新建client,编写package_test.go将上面的内容引入:

package client

import (
	"go_code/chapter2/series"
	"testing"
)

func TestPackage(t *testing.T) {
	t.Log(series.GetFibonacciSerie(5)) //输出结果为:[1,1,2,3,5]
	t.Log(series.Square(5))  //输出结果为:25
}

测试输出结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestPackage$ go_code/chapter2/client

init1
init2
=== RUN   TestPackage
    d:\Microsoft VS Code\GOproject\src\go_code\chapter2\client\package_test.go:9: [1 1 2 3 5]
    d:\Microsoft VS Code\GOproject\src\go_code\chapter2\client\package_test.go:10: 25
--- PASS: TestPackage (0.00s)
PASS
ok      go_code/chapter2/client 0.445s

通过在 chapter2 目录下创建一个名为 series 的包,把与数学相关的函数(如求平方和斐波那契数列)集中在一起。这样在其他地方需要使用这些功能时,只需引入这个包即可,不必重复编写相同的代码。

知识点:包的初始化

  • 利用 Go 语言中的 init() 函数机制进行包的初始化操作。在 Go 中,每个包可以有多个 init() 函数,这些函数会在包第一次被加载时自动执行,且执行顺序按照代码顺序。
  • series 包中编写了两个 init() 函数,它们会在包被引入时自动执行。这种机制可以用于在包加载时执行一些必要的初始化工作(如设置默认值、加载配置等),或者用来调试包的加载过程。

导入和应用远程依赖(即外部包)

获取和更新远程依赖

  • 使用 go get 命令来下载并添加远程依赖到项目中。Go Modules 会自动管理这些依赖,并更新 go.modgo.sum 文件。
  • 如果需要强制从网络获取最新版本的依赖,可以使用 -u 参数:
    • 示例go get -u github.com/user/repo
      这将更新指定包及其依赖项到最新的次要版本或修订版本。

代码在 GitHub 上的组织形式

  • 确保代码库的目录结构直接反映包的导入路径,而不要使用 src 目录作为根目录。这使得项目更容易与 Go 的依赖管理工具兼容,确保导入路径的简洁和一致性。
github.com/username/project/
├── mypackage/
│   └── mypackage.go
└── anotherpackage/
    └── anotherpackage.go
  • 最佳实践:在 GitHub 上组织代码时,目录结构应与包名匹配,例如:
  • 这样可以避免导入路径中的多余层级,并确保使用 go get 时能正确定位包。

按照该思路我们进行验证,在在 chapter7 目录下创建一个名为 remote_package 的包,我们先进行下载**“go get github.com/easierway/concurrent_map”**的下载,然后创建remote_package_test.go进行验证:

package remote

import (
	"fmt"
	"testing"

	// 并发安全Map库(别名cm)
	cm "github.com/easierway/concurrent_map"
)

func TestConcurrentMap(t *testing.T) {
	// 1. 创建并发Map(分片数=99)
	m := cm.CreateConcurrentMap(99)

	// 2. 插入键值对
	m.Set(cm.StrKey("key"), 10)

	// 3. 查询操作
	value, ok := m.Get(cm.StrKey("key"))

	// 4. 结果断言
	if ok {
		fmt.Println("Key found:", value) // 控制台输出(测试时慎用)
		t.Log(m.Get(cm.StrKey("key")))   // 推荐:记录到测试日志
	}
}

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestConcurrentMap$ go_code/chapter2/package/remote

=== RUN   TestConcurrentMap
Key found: 10
    d:\Microsoft VS Code\GOproject\src\go_code\chapter2\package\remote\remote_test.go:16: 10 true
--- PASS: TestConcurrentMap (0.00s)
PASS
ok      go_code/chapter2/package/remote 0.091s

concurrent_map的介绍:concurrent_map 是一个由 GitHub 用户 easierway 创建的 Go 包,主要用于实现线程安全的并发 map 数据结构。这个包提供了一种简单且高效的方式来处理并发环境下的 map 操作,避免了传统 map 在多 goroutine 访问时出现的竞争问题。

功能/特点 说明
线程安全 通过**分段锁机制(分片锁)**确保 map 在多 goroutine 并发访问时的数据安全。
高效的读写操作 将 map 分成多个子 map,减少锁的粒度,提高并发访问的效率。
简单易用的 API 提供类似标准 map 的接口,如 SetGetRemove,使用方式简单。
动态扩展 根据使用需求动态扩展或收缩分段,提高资源利用率。

包的依赖管理

Go 语言在早期的依赖管理中(使用 GOPATH)确实存在一些未解决的问题:

同一环境下,不同项目使用同一包的不同版本

在 Go Modules 引入之前,Go 的依赖管理依赖于 GOPATH 目录。所有的项目共享同一个 GOPATH,这就导致了一个问题:如果两个项目需要使用同一包的不同版本,由于 GOPATH 中同一个包只能有一个版本,无法同时满足这两个项目的需求。这种情况下,开发者往往需要手动管理和切换包版本,带来了很大的麻烦和不确定性。

无法管理对包的特定版本的依赖

在没有 Go Modules 之前,Go 的依赖管理缺乏对包版本的精确控制。通常情况下,开发者只能获取最新版本的包,这就导致了以下问题:

  • 当某个包发布了不兼容的新版本时,项目可能会因自动升级到新版本而导致编译或运行错误。
  • 难以重现历史版本的构建,因为无法确定项目依赖的具体版本。

Go Modules 如何解决这些问题

为了解决这些问题,Go 从 1.11 版本开始引入了 Go Modules,从根本上改变了 Go 的依赖管理方式。Go Modules 提供了版本控制和模块隔离的机制,避免了上述问题。

不同项目使用同一包的不同版本

  • 独立的模块空间:每个 Go 项目通过 go.mod 文件独立管理其依赖关系。go.mod 文件定义了项目所依赖的所有包及其版本,这些包会被下载到 $GOPATH/pkg/mod 下,并且是根据模块名和版本号来隔离的。因此,不同项目可以使用同一包的不同版本,而不会相互干扰。
  • 无需全局 GOPATH:Go Modules 摆脱了对全局 GOPATH 的依赖,转而使用模块级的依赖管理。每个项目的依赖包版本在项目目录下独立管理,避免了版本冲突。

管理对包的特定版本的依赖

  • 精确的版本控制:在 go.mod 文件中,你可以指定依赖包的具体版本。Go Modules 支持语义化版本控制(Semantic Versioning),你可以通过 @ 符号指定某个依赖包的版本号(如 v1.2.3),或者使用 go get <package>@<version> 命令来更新某个依赖的版本。这样,你可以明确指定和锁定项目依赖的版本,确保项目的可重现性。
  • 版本兼容性和依赖解析:Go Modules 通过 go.modgo.sum 文件管理版本依赖,确保项目构建过程中使用的依赖版本是可预测且稳定的。即使某个依赖包发布了新版本,你的项目仍会使用 go.mod 中指定的版本,除非你主动升级。

虽然 Go Modules 解决了许多依赖管理问题,但它也带来了一些新的挑战:

  • 多模块项目的管理:在一些大型项目中,可能会有多个模块,这些模块之间的依赖管理需要谨慎处理,特别是当这些模块之间存在依赖关系时。
  • 依赖冲突:如果不同的依赖项依赖于同一个包的不同版本,Go Modules 会尝试找到一个可用的共同版本,但这可能并不总是理想的解决方案。

Go Modules 通过模块化和版本控制,基本解决了 Go 语言早期依赖管理中的主要问题,如同一环境下不同项目使用同一包的不同版本,以及对包的特定版本的依赖管理问题。然而,尽管如此,随着项目规模的扩大和依赖关系的复杂化,依赖管理仍然需要开发者谨慎对待。

编写相同的代码。

知识点:包的初始化

  • 利用 Go 语言中的 init() 函数机制进行包的初始化操作。在 Go 中,每个包可以有多个 init() 函数,这些函数会在包第一次被加载时自动执行,且执行顺序按照代码顺序。
  • series 包中编写了两个 init() 函数,它们会在包被引入时自动执行。这种机制可以用于在包加载时执行一些必要的初始化工作(如设置默认值、加载配置等),或者用来调试包的加载过程。

导入和应用远程依赖(即外部包)

获取和更新远程依赖

  • 使用 go get 命令来下载并添加远程依赖到项目中。Go Modules 会自动管理这些依赖,并更新 go.modgo.sum 文件。
  • 如果需要强制从网络获取最新版本的依赖,可以使用 -u 参数:
    • 示例go get -u github.com/user/repo
      这将更新指定包及其依赖项到最新的次要版本或修订版本。

代码在 GitHub 上的组织形式

  • 确保代码库的目录结构直接反映包的导入路径,而不要使用 src 目录作为根目录。这使得项目更容易与 Go 的依赖管理工具兼容,确保导入路径的简洁和一致性。
github.com/username/project/
├── mypackage/
│   └── mypackage.go
└── anotherpackage/
    └── anotherpackage.go
  • 最佳实践:在 GitHub 上组织代码时,目录结构应与包名匹配,例如:
  • 这样可以避免导入路径中的多余层级,并确保使用 go get 时能正确定位包。

按照该思路我们进行验证,在在 chapter7 目录下创建一个名为 remote_package 的包,我们先进行下载**“go get github.com/easierway/concurrent_map”**的下载,然后创建remote_package_test.go进行验证:

package remote

import (
	"fmt"
	"testing"

	// 并发安全Map库(别名cm)
	cm "github.com/easierway/concurrent_map"
)

func TestConcurrentMap(t *testing.T) {
	// 1. 创建并发Map(分片数=99)
	m := cm.CreateConcurrentMap(99)

	// 2. 插入键值对
	m.Set(cm.StrKey("key"), 10)

	// 3. 查询操作
	value, ok := m.Get(cm.StrKey("key"))

	// 4. 结果断言
	if ok {
		fmt.Println("Key found:", value) // 控制台输出(测试时慎用)
		t.Log(m.Get(cm.StrKey("key")))   // 推荐:记录到测试日志
	}
}

[外链图片转存中…(img-lTFriOCz-1746605136776)]

测试输出:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestConcurrentMap$ go_code/chapter2/package/remote

=== RUN   TestConcurrentMap
Key found: 10
    d:\Microsoft VS Code\GOproject\src\go_code\chapter2\package\remote\remote_test.go:16: 10 true
--- PASS: TestConcurrentMap (0.00s)
PASS
ok      go_code/chapter2/package/remote 0.091s

concurrent_map的介绍:concurrent_map 是一个由 GitHub 用户 easierway 创建的 Go 包,主要用于实现线程安全的并发 map 数据结构。这个包提供了一种简单且高效的方式来处理并发环境下的 map 操作,避免了传统 map 在多 goroutine 访问时出现的竞争问题。

功能/特点 说明
线程安全 通过**分段锁机制(分片锁)**确保 map 在多 goroutine 并发访问时的数据安全。
高效的读写操作 将 map 分成多个子 map,减少锁的粒度,提高并发访问的效率。
简单易用的 API 提供类似标准 map 的接口,如 SetGetRemove,使用方式简单。
动态扩展 根据使用需求动态扩展或收缩分段,提高资源利用率。

包的依赖管理

Go 语言在早期的依赖管理中(使用 GOPATH)确实存在一些未解决的问题:

同一环境下,不同项目使用同一包的不同版本

在 Go Modules 引入之前,Go 的依赖管理依赖于 GOPATH 目录。所有的项目共享同一个 GOPATH,这就导致了一个问题:如果两个项目需要使用同一包的不同版本,由于 GOPATH 中同一个包只能有一个版本,无法同时满足这两个项目的需求。这种情况下,开发者往往需要手动管理和切换包版本,带来了很大的麻烦和不确定性。

无法管理对包的特定版本的依赖

在没有 Go Modules 之前,Go 的依赖管理缺乏对包版本的精确控制。通常情况下,开发者只能获取最新版本的包,这就导致了以下问题:

  • 当某个包发布了不兼容的新版本时,项目可能会因自动升级到新版本而导致编译或运行错误。
  • 难以重现历史版本的构建,因为无法确定项目依赖的具体版本。

Go Modules 如何解决这些问题

为了解决这些问题,Go 从 1.11 版本开始引入了 Go Modules,从根本上改变了 Go 的依赖管理方式。Go Modules 提供了版本控制和模块隔离的机制,避免了上述问题。

不同项目使用同一包的不同版本

  • 独立的模块空间:每个 Go 项目通过 go.mod 文件独立管理其依赖关系。go.mod 文件定义了项目所依赖的所有包及其版本,这些包会被下载到 $GOPATH/pkg/mod 下,并且是根据模块名和版本号来隔离的。因此,不同项目可以使用同一包的不同版本,而不会相互干扰。
  • 无需全局 GOPATH:Go Modules 摆脱了对全局 GOPATH 的依赖,转而使用模块级的依赖管理。每个项目的依赖包版本在项目目录下独立管理,避免了版本冲突。

管理对包的特定版本的依赖

  • 精确的版本控制:在 go.mod 文件中,你可以指定依赖包的具体版本。Go Modules 支持语义化版本控制(Semantic Versioning),你可以通过 @ 符号指定某个依赖包的版本号(如 v1.2.3),或者使用 go get <package>@<version> 命令来更新某个依赖的版本。这样,你可以明确指定和锁定项目依赖的版本,确保项目的可重现性。
  • 版本兼容性和依赖解析:Go Modules 通过 go.modgo.sum 文件管理版本依赖,确保项目构建过程中使用的依赖版本是可预测且稳定的。即使某个依赖包发布了新版本,你的项目仍会使用 go.mod 中指定的版本,除非你主动升级。

虽然 Go Modules 解决了许多依赖管理问题,但它也带来了一些新的挑战:

  • 多模块项目的管理:在一些大型项目中,可能会有多个模块,这些模块之间的依赖管理需要谨慎处理,特别是当这些模块之间存在依赖关系时。
  • 依赖冲突:如果不同的依赖项依赖于同一个包的不同版本,Go Modules 会尝试找到一个可用的共同版本,但这可能并不总是理想的解决方案。

Go Modules 通过模块化和版本控制,基本解决了 Go 语言早期依赖管理中的主要问题,如同一环境下不同项目使用同一包的不同版本,以及对包的特定版本的依赖管理问题。然而,尽管如此,随着项目规模的扩大和依赖关系的复杂化,依赖管理仍然需要开发者谨慎对待。


网站公告

今日签到

点亮在社区的每一天
去签到