干货分享,感谢您的阅读!备注:本博客将自己初步学习GO的总结进行分享,希望大家通过本博客可以在短时间内快速掌握GO的基本程序编码能力,如有错误请留言指正,谢谢!
一、初步了解Go语言
(一)Go语言诞生的主要问题和目标
-
多核硬件架构: 随着计算机硬件的发展,多核处理器成为主流,使得并行计算变得普遍。然而,传统的编程语言在处理多核并行性时可能面临困难,因为它们缺乏合适的原生支持。Go语言通过引入轻量级的协程(goroutine)和通道(channel)机制,使得并发编程变得更加容易。开发者可以轻松地创建数千个并发执行的协程,而无需担心线程管理的复杂性。
-
超大规模分布式计算集群: 随着云计算和分布式系统的崛起,构建和维护超大规模的分布式计算集群变得越来越常见。这些集群需要能够高效处理大量的请求、数据共享和协调。Go语言的并发特性和通道机制使得编写分布式系统变得更加容易,开发者可以使用协程和通道来处理并发任务、消息传递和协调工作。
-
Web模式导致的开发规模和更新速度增加: Web应用的兴起带来了前所未有的开发规模和持续更新的需求。传统的编程语言在开发大型Web应用时可能会面临可维护性、性能和开发效率等问题。Go语言通过其简洁的语法、高效的编译速度以及并发支持,使得开发者能够更快速地迭代和部署Web应用,同时也能够更好地处理高并发的网络请求。
综合来看,Go语言在诞生时确实着重解决了多核硬件架构、超大规模分布式计算集群和Web模式下的开发规模与速度等技术挑战,它的设计目标之一是提供一种适应现代软件开发需求的编程语言,使开发者能够更好地应对这些挑战。
(二)Go语言应用典型代表
Go语言在当下应用开发中已经得到广泛应用,许多知名公司和项目都使用Go语言来构建各种类型的应用。以下是一些代表性的产品和项目,它们使用了Go语言作为核心开发语言:
这些仅仅是Go语言应用的一小部分示例,实际上还有许多其他的项目和产品也在使用Go语言来构建高性能、可靠且易于维护的应用程序。这表明Go语言在现代应用开发中发挥了重要作用,特别是在分布式系统、云计算和高性能应用领域。
(三)Java、C++、C程序员在学习编写Go时存在的误区
当Java、C++、C等编程语言的程序员开始学习编写Go语言时,可能会遇到一些误区,因为Go在某些方面与这些传统语言有所不同。以下是一些常见的误区:
-
过度使用传统的并发模型: 传统的编程语言如Java、C++、C在处理并发时通常使用线程和锁来实现,但在Go中,使用协程(goroutine)和通道(channel)是更好的方式。新学习Go的程序员可能会继续使用传统的并发模型,而不充分利用Go的轻量级协程和通道,从而失去了Go的并发优势。
-
过度使用指针: C和C++等语言强调指针的使用,但Go语言在设计时避免了过多的指针操作。新学习Go的程序员可能会过度使用指针,导致代码变得复杂。在Go中,尽量避免使用指针,除非真正需要对值进行修改。
-
忽视错误处理: Go鼓励显式地处理错误,而不是简单地忽略它们。这与一些其他语言的习惯不同,其中错误往往被忽略或简单地抛出。新学习Go的程序员可能会忽视错误处理,导致潜在的问题未被检测到。
-
过度使用全局变量: 在C和C++等语言中,全局变量可能是常见的做法。然而,在Go中,全局变量的使用被视为不良实践。Go鼓励使用局部变量和传递参数的方式来传递数据,以避免引入不必要的耦合和副作用。
-
不熟悉切片和映射: Go中的切片和映射是强大的数据结构,但对于其他语言的程序员来说可能不太熟悉。学习如何正确使用切片和映射是很重要的,因为它们在Go中广泛用于集合和数据处理。
-
错误的Go风格: 每种语言都有其独特的编码风格和惯例。新学习Go的程序员可能会在Go代码中应用其他语言的编码风格,这可能会使代码难以阅读和理解。
为了避免这些误区,学习Go的程序员应该投入时间去理解Go语言的核心概念,包括并发模型、错误处理、数据结构等,同时积极参与Go社区,阅读Go的官方文档和示例代码,以便更好地适应Go的设计理念和最佳实践。
二、环境准备(以Mac说明)
(一)环境设置
在macOS上设置Go语言开发环境非常简单,可以按照以下步骤进行操作:
-
使用Homebrew安装: 如果您使用Homebrew包管理器,这是最方便的方法。打开终端,并运行以下命令来安装Go语言:
brew install go
-
手动安装: 如果想手动安装Go语言,可以按照以下步骤操作:
a. 访问官方网站下载安装包`goX.X.X.darwin-amd64.pkg
b. 双击下载的安装包,按照指示运行安装程序。按照默认设置即可,安装路径通常是
/usr/local/go
。 -
设置环境变量: 一旦安装完成,需要将Go语言的二进制路径添加到自己的终端配置文件中的PATH环境变量中。这样就可以在终端中直接运行Go命令。
a. 打开终端,并使用文本编辑器(如nano、vim或任何您喜欢的编辑器)编辑终端配置文件。例如:
nano ~/.bash_profile
b. 在文件中添加以下行(根据安装路径进行调整),然后保存并退出编辑器:
export PATH=$PATH:/usr/local/go/bin
c. 使配置生效,可以运行以下命令或者重启终端:
source ~/.bash_profile
-
验证安装: 打开终端,输入以下命令来验证Go是否已正确安装:
go version
如果看到了Go的版本号,表示安装成功。
(二)IDE选择说明
我个人使用的GoLand,直接官网下载后,上网购买破解版即可,这里不在多说!
三、Go语言程序学习
创建自己的工程目录/Users/zyf/zyfcodes/go/go-learning,新建src目录。
(一)第一个Go语言编写
src目录下创建chapter1/hello目录,新建hello.go文件,编写代码如下:
package mainimport ("fmt""os"
)/*** @author zhangyanfeng* @description 第一个godaima* @date 2023/8/20 23:45* @param* @return**/
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 ZYF”,运行结果为“Hello World ZYF”。
(二)基本程序结构编写学习
src目录下创建chapter2
1.变量
前提: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 variablesimport "testing"func TestFibList(t *testing.T) {a := 1b := 1t.Log(a)for i := 0; i < 5; i++ {t.Log(" ", b)tmp := aa = bb = tmp + a}
}func TestExchange(t *testing.T) {a := 1b := 2// tmp := a// a = b// b = tmpa, b = b, at.Log(a, b)
}
下面逐个解释代码中涉及的知识点:
-
package variables
: 声明了一个名为 "variables" 的包,这是一个用于测试的包名。 -
import "testing"
: 导入了Go语言的测试框架 "testing" 包,用于编写和运行测试函数。 -
在测试函数内部,声明了两个整数变量func TestFibList(t *testing.T) { ... }
: 定义了一个测试函数 "TestFibList",该函数用于测试斐波那契数列生成逻辑。这是一个测试函数的标准命名,以 "Test" 开头,接着是被测试的函数名。a
和b
,并将它们初始化为 1,这是斐波那契数列的前两个数。使用t.Log(a)
打印变量a
的值到测试日志中。使用循环来生成斐波那契数列的前 5 个数,每次迭代都会将b
的值打印到测试日志,并更新a
和b
的值以生成下一个数。 -
在测试函数内部,声明了两个整数变量func TestExchange(t *testing.T) { ... }
: 定义了另一个测试函数 "TestExchange",该函数用于测试变量交换的逻辑。a
和b
,并分别将它们初始化为 1 和 2。使用注释的方式展示了一种变量交换的写法(通过中间变量),但实际上被注释掉了。然后使用a, b = b, a
这一行代码来实现a
和b
的交换,这是Go语言中的一种特有的交换方式,不需要额外的中间变量。使用t.Log(a, b)
打印交换后的变量值到测试日志中。
2.常量
前提:chapter2目录下创建constant,学习总结如下:
- 常量声明: 使用
const
关键字声明一个常量,例如:const pi = 3.14159
。 - 常量赋值: 常量的值在声明时必须被赋值,一旦赋值后不可修改。
- 枚举常量: 可以使用一组常量来模拟枚举,例如:
const (Monday = 1Tuesday = 2// ... )
- 类型指定: 常量的类型也可以被指定,例如:
const speed int = 300000
。 - 常量表达式: 常量可使用表达式计算,例如:
const secondsInHour = 60 * 60
。 - 无类型常量: 常量可以是无类型的,根据上下文自动推断类型。例如,
const x = 5
会被推断为整数类型。
新建constant_test.go,写代码如下:
package constantimport "testing"const (Monday = 1 + iotaTuesdayWednesday
)const (Readable = 1 << iotaWritableExecutable
)func TestConstant1(t *testing.T) {t.Log(Monday, Tuesday)
}func TestConstant2(t *testing.T) {a := 1 //0001t.Log(a&Readable == Readable, a&Writable == Writable, 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)
打印常量Monday
和Tuesday
的值到测试日志中。 -
在测试函数内部,声明了一个整数变量func TestConstant2(t *testing.T) { ... }
: 定义了另一个测试函数 "TestConstant2",用于测试位运算和常量的使用。a
,并将其初始化为 1,即二进制中的 0001。使用位运算和按位与操作来检查变量a
是否具有Readable
、Writable
和Executable
属性。例如,a&Readable == Readable
表达式检查a
的二进制表示是否含有Readable
标志位。使用t.Log()
打印三个表达式的结果到测试日志中。
3.数据类型
前提:chapter2目录下创建 type,学习总结如下:
主要数据类型说明
Go语言具有丰富的内置数据类型,这些数据类型用于表示不同类型的值和数据。以下是对Go语言中一些主要数据类型的总结分析:
-
整数类型(Integer Types):Go语言提供不同大小的整数类型,如
int
、int8
、int16
、int32
和int64
。无符号整数类型有uint
、uint8
、uint16
、uint32
和uint64
。整数类型的大小取决于计算机的架构,例如32位或64位。 -
浮点数类型(Floating-Point Types):Go语言提供
float32
和float64
两种浮点数类型,分别对应单精度和双精度浮点数。 -
复数类型(Complex Types):Go语言提供
complex64
和complex128
两种复数类型,分别对应由两个浮点数构成的复数。 -
布尔类型(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 mainimport "fmt"type Person struct {FirstName stringLastName stringAge 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) intfunc main() {fmt.Println("整数类型(Integer Types)")var x int = 10var y int64 = 100fmt.Println(x)fmt.Println(y)fmt.Println("浮点数类型(Floating-Point Types)")var a float32 = 3.14var b float64 = 3.14159265359fmt.Println(a)fmt.Println(b)fmt.Println("布尔类型(Boolean Type)")var isTrue bool = truevar isFalse bool = falsefmt.Println(isTrue)fmt.Println(isFalse)fmt.Println("字符串类型(String Type)")str1 := "Hello, "str2 := "Go!"concatenated := str1 + str2fmt.Println(concatenated)fmt.Println("切片类型(Slice Types)")numbers := []int{1, 2, 3, 4, 5}fmt.Println(numbers)// 修改切片元素numbers[0] = 10fmt.Println(numbers)// 切片操作subSlice := numbers[1:4]fmt.Println(subSlice)fmt.Println("映射类型(Map Types)")ages := map[string]int{"Alice": 25,"Bob": 30,"Eve": 28,}fmt.Println(ages)fmt.Println("Alice's age:", ages["Alice"])// 添加新的键值对ages["Charlie"] = 22fmt.Println(ages)fmt.Println("结构体类型(Struct Types)")person := Person{FirstName: "John",LastName: "Doe",Age: 30,}fmt.Println(person)fmt.Println("Name:", person.FirstName, person.LastName)fmt.Println("接口类型(Interface Types)")var shape Shapecircle := Circle{Radius: 5}shape = circlefmt.Println("Circle Area:", shape.Area())fmt.Println("函数类型(Function Types)")var op Operationop = addresult := op(10, 5)fmt.Println("Addition:", result)op = subtractresult = op(10, 5)fmt.Println("Subtraction:", result)fmt.Println("通道类型(Channel Types)")messages := make(chan string)go func() {messages <- "Hello, Go!"}()msg := <-messagesfmt.Println(msg)fmt.Println("指针类型(Pointer Types)")x = 10var ptr *intptr = &xfmt.Println("Value of x:", x)fmt.Println("Value stored in pointer:", *ptr)*ptr = 20fmt.Println("Updated value of x:", x)
}
下面逐个解释代码中涉及的知识点:
-
type Person struct { ... }
: 定义了一个结构体类型Person
,表示一个人的信息,包括FirstName
、LastName
和Age
字段。 -
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 中发送和接收消息。
- 演示了指针的使用,包括创建指针变量、通过指针修改变量的值等操作。
Go语言中类型转换说明
Go语言支持类型转换,但需要注意一些规则和限制。类型转换用于将一个数据类型的值转换为另一个数据类型,以便在不同的上下文中使用。以下是有关Go语言中类型转换的一些重要信息:
-
基本类型之间的转换: 可以在基本数据类型之间进行转换,但是必须注意类型的兼容性和可能导致的数据丢失。例如,从
int
到float64
的转换是安全的,但从float64
到int
可能导致小数部分被截断。 -
显示类型转换: 在Go中,使用强制类型转换来显式指定将一个值转换为另一个类型。语法是:
destinationType(expression)
。例如:float64(10)
。 -
非兼容类型之间的转换: 对于不兼容的类型,编译器不会自动进行转换。例如,不能直接将一个
string
类型转换为int
类型。 -
类型别名的转换: 如果有类型别名(Type Alias),在转换时需要注意使用别名的兼容性。
以下是一些示例来展示类型转换:
package mainimport "fmt"func main() {// 显式类型转换var x int = 10var y float64 = float64(x)fmt.Println(y)// 类型别名的转换type Celsius float64type Fahrenheit float64c := Celsius(25)f := Fahrenheit(c*9/5 + 32)fmt.Println(f)
}
4.运算符
前提:chapter2目录下创建 operator,学习总结如下:
其实这部分和其他语言都差不多,个人觉得没啥可复习巩固的。Go语言支持多种运算符,用于执行各种算术、逻辑和比较操作。
常规运算符
以下是一些常见的运算符及其在Go中的使用方式和知识点:
算术运算符(Arithmetic Operators):
+
:加法-
:减法*
:乘法/
:除法%
:取模(取余数)
赋值运算符(Assignment Operators):
=
:赋值+=
:加法赋值-=
:减法赋值*=
:乘法赋值/=
:除法赋值%=
:取模赋值
逻辑运算符(Logical Operators):
&&
:逻辑与(AND)||
:逻辑或(OR)!
:逻辑非(NOT)
比较运算符(Comparison Operators):
==
:等于!=
:不等于<
:小于>
:大于<=
:小于等于>=
:大于等于
位运算符(Bitwise Operators):
&
:按位与(AND)|
:按位或(OR)^
:按位异或(XOR)<<
:左移>>
:右移
其他运算符:
&
:取地址运算符*
:指针运算符++
:自增运算符--
:自减运算符
在使用运算符时,需要考虑以下几点:
- 运算符的操作数必须与运算符的预期类型匹配。
- 某些运算符具有更高的优先级,需要使用括号来明确优先级。
- 运算符的操作数可以是变量、常量、表达式等。
新建operator_test.go,以下是一些示例来展示运算符的使用:
package operatorimport ("fmt""testing"
)const (Readable = 1 << iotaWritableExecutable
)func TestOperatorBasic(t *testing.T) {// 算术运算符a := 10b := 5fmt.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 := truey := falsefmt.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 == c)t.Log(a == d)
}func TestBitClear(t *testing.T) {a := 7 //0111a = a &^ Readablea = a &^ Executablet.Log(a&Readable == Readable, a&Writable == Writable, a&Executable == Executable)
}
下面逐个解释代码中涉及的知识点:
-
const (...)
: 定义了三个常量Readable
、Writable
和Executable
,使用位移操作生成不同的值。 -
算术运算符:展示了加法、减法、乘法、除法和取余运算;逻辑运算符:展示了逻辑与、逻辑或和逻辑非运算;比较运算符:展示了等于、不等于、大于、小于、大于等于和小于等于运算。func TestOperatorBasic(t *testing.T) { ... }
: 定义了一个测试函数 "TestOperatorBasic",用于测试基本运算符的使用。 -
声明了两个整数数组func TestCompareArray(t *testing.T) { ... }
: 定义了一个测试函数 "TestCompareArray",用于测试数组的比较。a
和b
,以及另一个数组d
,其中数组a
和数组d
的内容相同;使用比较运算符==
检查数组a
和b
是否相等,以及数组a
和d
是否相等。 -
声明一个整数变量func TestBitClear(t *testing.T) { ... }
: 定义了一个测试函数 "TestBitClear",用于测试位清除操作。a
,并将其初始化为7
,即二进制表示0111;
使用位清除操作&^
将a
中的Readable
和Executable
位清除;使用按位与运算&
检查a
是否具有Readable
、Writable
和Executable
属性。
按位清除运算符 &^
在Go语言中,&^
是按位清除运算符(Bit Clear Operator)。它用于将某些位置上的位清零,即将指定位置上的位设置为0。&^
运算符在处理二进制位操作时非常有用。
&^
运算符执行以下操作:
- 对于每个位,如果右侧操作数的对应位为 0,则结果位与左侧操作数相同。
- 对于每个位,如果右侧操作数的对应位为 1,则结果位被强制设置为 0。
这意味着,&^
运算符用于“清除”左侧操作数的特定位,使其与右侧操作数的相应位不受影响。写个代码验证下:
func TestOther(t *testing.T) {var a uint8 = 0b11001100 // 二进制表示,十进制为 204var b uint8 = 0b00110011 // 二进制表示,十进制为 51result := a &^ bfmt.Printf("a: %08b\n", a) // 输出:11001100fmt.Printf("b: %08b\n", b) // 输出:00110011fmt.Printf("Result: %08b\n", result) // 输出:11000000fmt.Println("Result (Decimal):", result) // 输出:192
}
5.条件语句(Conditional Statements)
前提:chapter2目录下创建 condition,学习总结如下:
if
语句
if
语句用于基于条件来决定是否执行某段代码。它的基本语法如下:
if condition {// 代码块
} else if anotherCondition {// 代码块
} else {// 代码块
}
switch
语句
switch
语句用于基于表达式的不同值执行不同的代码分支。与其他语言不同,Go的switch
可以自动匹配第一个满足条件的分支,而无需使用break
语句。它的语法如下:
switch expression {
case value1:// 代码块
case value2:// 代码块
default:// 代码块
}
创建condition_test.go进行验证分析, 具体代码如下:
package conditionimport ("fmt""testing"
)func TestConditionIf(t *testing.T) {age := 18if age < 18 {fmt.Println("You are a minor.")} else if age >= 18 && age < 60 {fmt.Println("You are an adult.")} else {fmt.Println("You are a senior citizen.")}
}func TestConditionSwitch(t *testing.T) {dayOfWeek := 3switch dayOfWeek {case 1:fmt.Println("Monday")case 2:fmt.Println("Tuesday")case 3:fmt.Println("Wednesday")case 4:fmt.Println("Thursday")case 5:fmt.Println("Friday")default:fmt.Println("Weekend")}
}func TestSwitchMultiCase(t *testing.T) {for i := 0; i < 5; i++ {switch i {case 0, 2:t.Logf("%d is Even", i)case 1, 3:t.Logf("%d is Odd", i)default:t.Logf("%d is not 0-3", i)}}
}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
语句的使用。if
、else if
和else
分支判断是否为未成年人、成年人或老年人。 -
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
语句中使用条件表达式的情况。
6.循环语句(Loop Statements)
前提:chapter2目录下创建 loop,学习总结如下:
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 loopimport ("fmt""testing"
)func TestLoopFor(t *testing.T) {for i := 1; i <= 5; i++ {fmt.Println("Iteration:", i)}
}func TestLoopForBasic(t *testing.T) {i := 1for 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 := 1for {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
循环的用法,包括标准的计数循环、不带初始化语句的循环、遍历切片以及无限循环与循环终止条件。
7.跳转语句(Jump Statements)
前提:chapter2目录下创建 jump,学习总结如下:
Go语言也支持几种跳转语句,用于在循环和条件中控制流程:
break
:跳出循环。continue
:跳过本次循环迭代,继续下一次迭代。goto
:在代码中直接跳转到指定标签处(不推荐使用)。
创建jump_test.go进行验证分析, 具体代码如下:
package jumpimport ("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 := 1start: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
语句实现了一个无限循环,即使用标签start
和goto start
在循环体内部跳转到循环的起始位置。循环的终止条件是当i
大于 5 时。
这些测试函数展示了Go语言中的循环控制跳转语句,包括用于终止循环的 break
、用于跳过当前迭代的 continue
,以及用于无限循环的 goto
语句。
(三)常用集合和字符串
src目录下创建chapter3,在Go语言中,集合是存储一组值的数据结构。常用的集合类型包括数组、切片、映射和通道。
1.数组
前提:chapter3目录下创建 array,学习总结如下:
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)
}
数组作为函数参数
数组在函数参数传递时会创建副本,因此对函数内的数组修改不会影响原始数组。如果需要在函数内修改原始数组,可以传递指向数组的指针。
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},
}
数组在存储固定数量的同类型元素时非常有用,但由于其固定长度的限制,通常在实际开发中更常用的是切片,它具有动态长度的特性。切片可以根据需要进行增加、删除和重新分配,更加灵活。
创建array_test.go进行验证分析, 具体代码如下:
package arrayimport "testing"func TestArrayInit(t *testing.T) {var arr [3]intarr1 := [4]int{1, 2, 3, 4}arr3 := [...]int{1, 3, 4, 5}arr1[1] = 5t.Log(arr[1], arr[2])t.Log(arr1, arr3)
}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) { ... }
:测试数组的初始化。arr
,arr1
和arr3;
修改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
的内容。
2.切片
前提:chapter3目录下创建 slice,学习总结如下:
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 sliceimport ("fmt""testing"
)func TestSlice(t *testing.T) {// 声明和初始化切片numbers := []int{1, 2, 3, 4, 5}fmt.Println("Original Slice:", numbers)// 使用 make 函数创建切片slice := make([]int, 3)fmt.Println("Initial Make Slice:", slice)// 添加元素到切片slice = append(slice, 10)slice = append(slice, 20, 30)fmt.Println("After Append:", slice)// 复制切片copySlice := make([]int, len(slice))copy(copySlice, slice)fmt.Println("Copied Slice:", copySlice)// 切片切割subSlice := numbers[1:3]fmt.Println("Subslice:", subSlice)// 修改切片的值会影响底层数组和其他切片subSlice[0] = 100fmt.Println("Modified Subslice:", subSlice)fmt.Println("Original Slice:", numbers)fmt.Println("Copied Slice:", copySlice)// 遍历切片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语言中切片的各种操作,包括切片的创建、添加元素、复制切片、切片切割、修改切片元素等。
3.Map
前提:chapter3目录下创建 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_mapimport ("fmt""testing"
)func TestBasic(t *testing.T) {// 声明和初始化映射ages := map[string]int{"Alice": 25,"Bob": 30,"Eve": 28,}fmt.Println("Original Map:", ages)// 添加新的键值对ages["Charlie"] = 35fmt.Println("After Adding:", ages)// 修改已有键的值ages["Bob"] = 31fmt.Println("After Modification:", ages)// 删除键值对delete(ages, "Eve")fmt.Println("After Deletion:", ages)// 获取值和检查键是否存在age, exists := ages["Alice"]if exists {fmt.Println("Alice's Age:", age)} else {fmt.Println("Alice not found")}// 遍历映射for name, age := range ages {fmt.Printf("Name: %s, Age: %d\n", name, age)}
}type Student struct {Name stringAge intGrade 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"] = 95studentInfo["Bob"] = Student{Name: "Bob", Age: 19, Grade: "B"}studentScores["Bob"] = 85// 查找学生信息和成绩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, exists := studentScores[name]if exists {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语言中映射的各种操作,包括创建、添加、修改、删除键值对,检查键是否存在,以及遍历映射的键值对。
4.实现Set
前提:chapter3目录下创建 set,学习总结如下:
在Go语言中,虽然标准库没有提供内置的Set类型,但你可以使用多种方式来实现Set的功能。以下是几种常见的实现Set的方式介绍:
使用切片
创建set_slice_test.go练习
使用切片来存储元素,通过遍历切片来检查元素是否存在。这是一个简单的实现方式,适用于小型的集合。
package setimport ("fmt""testing"
)type IntSet struct {elements []int
}func (s *IntSet) Add(element int) {if !s.Contains(element) {s.elements = append(s.elements, element)}
}func (s *IntSet) Contains(element int) bool {for _, e := range s.elements {if e == element {return true}}return false
}func TestSet(t *testing.T) {set := IntSet{}set.Add(1)set.Add(2)set.Add(3)set.Add(2) // Adding duplicate, should be ignoredfmt.Println("Set:", set.elements) // Output: [1 2 3]
}
使用映射
创建set_map_test.go练习
使用映射来存储元素,映射的键代表集合的元素,值可以是任意类型。这样的实现方式更快速,适用于大型的集合,因为映射的查找复杂度为 O(1)。
package setimport ("fmt""testing"
)type Set map[int]boolfunc (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)set.Add(2) // Adding duplicate, should be ignoredfmt.Println("Set:", set) // Output: map[1:true 2:true 3:true]
}
使用第三方库
创建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
package setimport ("fmt""github.com/deckarep/golang-set""testing"
)func TestSetThird(t *testing.T) {intSet := mapset.NewSet()intSet.Add(1)intSet.Add(2)intSet.Add(3)intSet.Add(2) // Adding duplicate, will be ignoredfmt.Println("Set:", intSet) // Output: Set: Set{1, 2, 3}
}
以上是几种实现Set的方式,你可以根据需求和性能考虑选择适合的实现方式。第三方库可以提供更多功能和性能优化,适用于大规模的数据集合。
5.字符串
前提:chapter3目录下创建 string,学习总结如下:
字符串的声明与初始化
在Go语言中,字符串是由一系列字符组成的,可以使用双引号 "
或反引号 ``` 来声明和初始化字符串。
package mainimport "fmt"func main() {str1 := "Hello, World!" // 使用双引号声明str2 := `Go Programming` // 使用反引号声明fmt.Println(str1) // Output: Hello, World!fmt.Println(str2) // Output: Go Programming
}
字符串的长度
使用内置函数 len()
可以获取字符串的长度,即字符串中字符的个数。
package mainimport "fmt"func main() {str := "Hello, 世界!"length := len(str)fmt.Println("String Length:", length) // Output: String Length: 9
}
字符串的索引与切片
字符串中的字符可以通过索引访问,索引从0开始。可以使用切片操作来获取字符串的子串。
package mainimport "fmt"func main() {str := "Hello, World!"// 获取第一个字符firstChar := str[0]fmt.Println("First Character:", string(firstChar)) // Output: First Character: H// 获取子串substring := str[7:12]fmt.Println("Substring:", substring) // Output: Substring: World
}
字符串拼接
使用 +
运算符可以将两个字符串连接成一个新的字符串。另外,strings.Join
函数用于将字符串切片连接成一个新的字符串,可以用来拼接多个字符串。
最后,使用字节缓冲可以在不产生多余字符串副本的情况下进行高效的字符串拼接。
package mainimport ("fmt""strings""bytes"
)func main() {str1 := "Hello, "str2 := "World!"result := str1 + str2fmt.Println("Concatenated String:", result) // Output: Concatenated String: Hello, World!strSlice := []string{"Hello", " ", "World!"}result := strings.Join(strSlice, "")fmt.Println(result) // Output: Hello World!var buffer bytes.Bufferbuffer.WriteString(str1)buffer.WriteString(str2)result := buffer.String()fmt.Println(result) // Output: Hello, World!
}
多行字符串
使用反引号 ``` 来创建多行字符串。
package mainimport "fmt"func main() {multiLineStr := `This is amulti-linestring.`fmt.Println(multiLineStr)
}
字符串迭代
使用 for range
循环迭代字符串的每个字符。
package mainimport "fmt"func main() {str := "Go语言"for _, char := range str {fmt.Printf("%c ", char) // Output: G o 语 言}
}
字符串和字节数组之间的转换
在Go语言中,字符串和字节数组之间可以进行相互转换。
package mainimport "fmt"func main() {str := "Hello"bytes := []byte(str) // 转换为字节数组strAgain := string(bytes) // 字节数组转换为字符串fmt.Println("Bytes:", bytes) // Output: Bytes: [72 101 108 108 111]fmt.Println("String Again:", strAgain) // Output: String Again: Hello
}
字符串比较
字符串的比较可以使用 ==
和 !=
运算符。当然还有其他函数类型的直接应用的:strings.Compare
函数用于比较两个字符串,并根据比较结果返回一个整数。
也可以使用自定义的比较函数来比较字符串,根据自己的需求定义比较逻辑。
package mainimport ("fmt""strings"
)func customCompare(str1, str2 string) bool {// 自定义比较逻辑return str1 == str2
}func main() {str1 := "Hello"str2 := "World"if str1 == str2 {fmt.Println("Strings are equal")} else {fmt.Println("Strings are not equal") // Output: Strings are not equal}result := strings.Compare(str1, str2)if result == 0 {fmt.Println("Strings are equal")} else if result < 0 {fmt.Println("str1 is less than str2")} else {fmt.Println("str1 is greater than str2") // Output: str1 is less than str2}if customCompare(str1, str2) {fmt.Println("Strings are equal")} else {fmt.Println("Strings are not equal") // Output: Strings are not equal}
}
这些基本概念和操作可以帮助你更好地理解和使用Go语言中的字符串。要注意字符串的不可变性,以及与其他数据类型的转换和比较。
创建string_test.go练习
package stringimport ("strconv""strings""testing"
)func TestString(t *testing.T) {var s stringt.Log(s) //初始化为默认零值“”s = "hello"t.Log(len(s))//s[1] = '3' //string是不可变的byte slice//s = "\xE4\xB8\xA5" //可以存储任何二进制数据s = "\xE4\xBA\xBB\xFF"t.Log(s)t.Log(len(s))s = "中"t.Log(len(s)) //是byte数c := []rune(s)t.Log(len(c))// t.Log("rune size:", unsafe.Sizeof(c[0]))t.Logf("中 unicode %x", c[0])t.Logf("中 UTF8 %x", s)
}func TestStringToRune(t *testing.T) {s := "中华人民共和国"for _, c := range s {t.Logf("%[1]c %[1]x", c)}
}func TestStringFn(t *testing.T) {s := "A,B,C"parts := strings.Split(s, ",")for _, part := range parts {t.Log(part)}t.Log(strings.Join(parts, "-"))
}func TestConv(t *testing.T) {s := strconv.Itoa(10)t.Log("str" + s)if i, err := strconv.Atoi("10"); err == nil {t.Log(10 + i)}
}
下面逐个解释每个测试函数的内容:
-
声明一个字符串变量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
类型转换、字符串拆分和合并,以及字符串与其他类型的转换。
(四)函数
src目录下创建chapter4,在Go语言中,函数是一种用于执行特定任务的代码块,可以被多次调用。
1.函数的声明
在Go中,函数的声明由关键字 func
开始,后面跟着函数名、参数列表、返回值和函数体。
func functionName(parameters) returnType {// 函数体// 可以包含多个语句return returnValue
}
2.函数参数
函数可以有零个或多个参数,参数由参数名和参数类型组成。参数之间使用逗号分隔。
func greet(name string) {fmt.Printf("Hello, %s!\n", name)
}
3.多返回值
Go语言的函数可以返回多个值。返回值用括号括起来,逗号分隔。
func divide(a, b float64) (float64, error) {if b == 0 {return 0, errors.New("division by zero")}return a / b, nil
}
4.命名返回值
函数可以声明命名的返回值,在函数体内可以直接使用这些名称进行赋值,最后不需要显式使用 return
关键字。
func divide(a, b float64) (result float64, err error) {if b == 0 {err = errors.New("division by zero")return}result = a / breturn
}
5.可变数量的参数
Go语言支持使用 ...
语法来表示可变数量的参数。这些参数在函数体内作为切片使用。
func sum(numbers ...int) int {total := 0for _, num := range numbers {total += num}return total
}
6.函数作为参数
在Go语言中,函数可以作为参数传递给其他函数。
func applyFunction(fn func(int, int) int, a, b int) int {return fn(a, b)
}func add(a, b int) int {return a + b
}func main() {result := applyFunction(add, 3, 4)fmt.Println(result) // Output: 7
}
7.匿名函数和闭包
Go语言支持匿名函数,也称为闭包。这些函数可以在其他函数内部定义,并访问外部函数的变量。
func main() {x := 5fn := func() {fmt.Println(x) // 闭包访问外部变量}fn() // Output: 5
}
8.defer语句
defer
语句用于延迟执行函数,通常用于在函数返回前执行一些清理操作。
func main() {defer fmt.Println("World")fmt.Println("Hello")
}
以上是一些关于Go语言函数的基本知识点。函数在Go中扮演着非常重要的角色,用于组织代码、实现功能模块化和提高代码的可维护性。
验证一:基本使用用例验证
在chapter4下新建basic,在创建func_basic_test.go练习
package basicimport ("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 zero")}return a / b, nil
}// 命名返回值函数
func divideNamed(a, b int) (result int, err error) {if b == 0 {err = errors.New("division by zero")return}result = a / breturn
}// 可变数量的参数函数
func sum(numbers ...int) int {total := 0for _, 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 := 5fn := func() {fmt.Println(x)}fn() // Output: 5
}// defer语句
func deferExample() {defer fmt.Println("World")fmt.Println("Hello") // Output: Hello World
}func TestBasic(t *testing.T) {greet("Alice") // Output: Hello, Alice!q, err := divide(10, 2)if err != nil {fmt.Println("Error:", err)} else {fmt.Println("Quotient:", q) // Output: Quotient: 5}qNamed, errNamed := divideNamed(10, 0)if errNamed != nil {fmt.Println("Error:", errNamed) // Output: Error: division by zero} else {fmt.Println("Quotient:", qNamed)}total := sum(1, 2, 3, 4, 5)fmt.Println("Sum:", total) // Output: Sum: 15addResult := applyFunction(func(a, b int) int {return a + b}, 3, 4)fmt.Println("Addition:", addResult) // Output: Addition: 7closureExample()deferExample()
}
验证二:业务小举例
在chapter4下新建biz,在创建func_biz_test.go练习,假设你正在开发一个简单的订单处理系统,需要计算订单中商品的总价和应用折扣。你可以使用函数来处理这些业务逻辑。以下是一个简单的示例:
package bizimport ("fmt""testing"
)type Product struct {Name stringPrice float64
}func calculateTotal(products []Product) float64 {total := 0.0for _, p := range products {total += p.Price}return total
}func applyDiscount(amount, discount float64) float64 {return amount * (1 - discount)
}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},}total := calculateTotal(products)fmt.Printf("Total before discount: $%.2f\n", total)discountedTotal := applyDiscount(total, 0.1)fmt.Printf("Total after 10%% discount: $%.2f\n", discountedTotal)
}
(五)面向对象编程
src目录下创建chapter5,Go语言支持面向对象编程(Object-Oriented Programming,OOP),尽管与一些传统的面向对象编程语言(如Java和C++)相比,Go的实现方式可能略有不同。在Go语言中,没有类的概念,但可以通过结构体和方法来实现面向对象的特性。
1.结构体定义
在Go语言中,结构体是一种自定义的数据类型,用于组合不同类型的字段(成员变量)以创建一个新的数据类型。创建struct目录,编写struct_test.go,以下是结构体的定义、使用和验证示例:
package _structimport ("fmt""testing"
)// 定义一个结构体
type Person struct {FirstName stringLastName stringAge int
}func TestStruct(t *testing.T) {// 创建结构体实例并初始化字段person1 := Person{FirstName: "Alice",LastName: "Smith",Age: 25,}// 访问结构体字段fmt.Println("First Name:", person1.FirstName) // Output: First Name: Alicefmt.Println("Last Name:", person1.LastName) // Output: Last Name: Smithfmt.Println("Age:", person1.Age) // Output: Age: 25// 修改结构体字段的值person1.Age = 26fmt.Println("Updated Age:", person1.Age) // Output: Updated Age: 26
}
结构体的定义可以包含多个字段,每个字段可以是不同的数据类型。你还可以在结构体中嵌套其他结构体,形成更复杂的数据结构。编写struct_cmpx_test.go示例:
package _structimport ("fmt""testing"
)type Address struct {Street stringCity stringZipCode string
}type PersonNew struct {FirstName stringLastName stringAge intAddress 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)
}
2.实例创建及初始化
在Go语言中,可以通过多种方式来创建和初始化结构体实例。创建creatinit目录,以下是几种常见的实例创建和初始化方法,具体代码为creatinit_test.go
- 字面量初始化:可以使用花括号
{}
来初始化结构体实例的字段。 - 部分字段初始化: 如果你只想初始化结构体的部分字段,可以省略其他字段。
- 使用字段名初始化: 可以根据字段名来指定字段的值,无需按顺序初始化。
- 默认值初始化: 结构体的字段可以根据其类型的默认值进行初始化。
- 使用 new 函数: 可以使用
new
函数来创建一个指向结构体的指针,并返回其指针。 - 字段顺序初始化: 可以选择性地省略字段名,但是这时候需要按照结构体字段的顺序进行赋值。
package creatinitimport ("fmt""testing"
)type Person struct {FirstName stringLastName stringAge int
}/*** @author zhangyanfeng* @description 字面量初始化* @date 2023/8/26 15:09**/
func TestCreateObj1(t *testing.T) {person1 := Person{FirstName: "Alice",LastName: "Smith",Age: 25,}fmt.Println(person1.FirstName, person1.LastName, person1.Age) // Output: Alice Smith 25
}/*** @author zhangyanfeng* @description 部分字段初始化* @date 2023/8/26 15:10**/
func TestCreateObj2(t *testing.T) {person2 := Person{FirstName: "Bob",Age: 30,}fmt.Println(person2.FirstName, person2.LastName, person2.Age) // Output: Bob 30
}/*** @author zhangyanfeng* @description 使用字段名初始化* @date 2023/8/26 15:12**/
func TestCreateObj3(t *testing.T) {person3 := Person{LastName: "Johnson",FirstName: "Chris",Age: 28,}fmt.Println(person3.FirstName, person3.LastName, person3.Age) // Output: Chris Johnson 28
}/*** @author zhangyanfeng* @description 默认值初始化* @date 2023/8/26 15:13**/
func TestCreateObj4(t *testing.T) {var person4 Personfmt.Println(person4.FirstName, person4.LastName, person4.Age) // Output: 0
}/*** @author zhangyanfeng* @description 使用 new 函数* @date 2023/8/26 15:14**/
func TestCreateObj5(t *testing.T) {person5 := new(Person)person5.FirstName = "David"person5.Age = 22fmt.Println(person5.FirstName, person5.LastName, person5.Age) // Output: David 22
}/*** @author zhangyanfeng* @description 字段顺序初始化* @date 2023/8/26 15:24**/
func TestCreateObj6(t *testing.T) {// 使用字段顺序初始化person := Person{"Alice", "Smith", 25}fmt.Println(person.FirstName, person.LastName, person.Age) // Output: Alice Smith 25
}
3.行为(方法)定义
在Go语言中,方法是与特定类型相关联的函数,它可以在这个类型的实例上调用。方法使得类型的操作能够与该类型的定义放在一起,提高了代码的可读性和可维护性。
创建method目录进行代码练习,以下是关于Go语言方法的定义、使用和分析:
方法的定义
在Go语言中,方法是通过为函数添加接收者(receiver)来定义的。接收者是一个普通的参数,但它在方法名前放置,用于指定该方法与哪种类型相关联。创建method_define_test.go
package methodimport ("fmt""testing"
)type Circle struct {Radius float64
}// 定义 Circle 类型的方法
func (c Circle) Area() float64 {return 3.14159 * c.Radius * c.Radius
}func TestMethodDef(t *testing.T) {c := Circle{Radius: 5}area := c.Area()fmt.Printf("Circle area: %.2f\n", area) // Output: Circle area: 78.54
}
在上述示例中,我们定义了一个 Circle
结构体,然后为其定义了一个名为 Area
的方法。这个方法可以通过 c.Area()
的方式调用,其中 c
是一个 Circle
类型的实例。
方法的调用
方法调用的语法为 实例.方法名()
,即通过实例来调用方法。创建method_rpc_test.go
package methodimport ("fmt""testing"
)type Rectangle struct {Width float64Height float64
}func (r Rectangle) Area() float64 {return r.Width * r.Height
}func TestMethonRpc(t *testing.T) {rect := Rectangle{Width: 3, Height: 4}area := rect.Area()fmt.Printf("Rectangle area: %.2f\n", area) // Output: Rectangle area: 12.00
}
指针接收者
Go语言支持使用指针作为方法的接收者,这样可以修改接收者实例的字段值。创建method_rec_test.go
package methodimport ("fmt""testing"
)type Counter struct {Count int
}func (c *Counter) Increment() {c.Count++
}func TestMethonRec(t *testing.T) {counter := Counter{Count: 0}counter.Increment()fmt.Println("Count:", counter.Count) // Output: Count: 1
}
在上述示例中,Increment
方法使用了指针接收者,这样调用方法后,Count
字段的值会被修改。
方法与函数的区别
方法与函数的主要区别在于方法是特定类型的函数,它与类型的关系更加紧密,可以访问类型的字段和其他方法。函数则是独立于特定类型的代码块。方法通常用于实现特定类型的行为,而函数可以用于通用的操作。
通过定义方法,你可以使类型的操作更加自然和一致,提高代码的可读性和模块化。
这里可以说明一下,method_rpc_test.go中,我们为 Rectangle
结构体定义了一个名为 Area
的方法,该方法可以通过 rect.Area()
的方式调用。方法直接与类型 Rectangle
关联,可以访问 Rectangle
的字段(Width
和 Height
)。
我们为了与方法作对比,在对应方法体中创建一个方法如下
// 定义一个函数来计算矩形的面积
func CalculateArea(r Rectangle) float64 {return r.Width * r.Height
}
在这个示例中,我们定义了一个名为 CalculateArea
的函数,它接受一个 Rectangle
类型的参数来计算矩形的面积。函数是独立于 Rectangle
类型的,因此它无法直接访问 Rectangle
的字段。
总结: 方法与函数的区别在于方法是特定类型的函数,与类型的关系更加紧密,可以访问类型的字段和其他方法。而函数是独立于特定类型的代码块,通常用于通用的操作。在上述示例中,方法与矩形相关联,可以直接访问矩形的字段;函数则是一个独立的计算过程,不与任何特定类型直接关联。
通过使用方法,我们可以使代码更加自然和一致,提高代码的可读性和模块化,特别是在实现特定类型的行为时。
4.接口定义使用
在Go语言中,接口是一种定义方法集合的方式,它规定了一组方法的签名,而不涉及实现细节。通过接口,可以实现多态性和代码解耦,使不同类型的对象能够按照一致的方式进行操作。
创建interface目录用于后续练习,以下是关于Go语言接口的讲解:
定义接口
接口是一组方法的集合,通过 type
关键字定义。接口定义了一组方法签名,但不包含方法的实现。创建interface_test.go进行代码练习
package interface_testimport ("fmt""testing"
)// 定义一个简单的接口
type Shape interface {Area() float64
}// 定义两个实现 Shape 接口的结构体
type Circle struct {Radius float64
}func (c Circle) Area() float64 {return 3.14159 * c.Radius * c.Radius
}type Rectangle struct {Width float64Height 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())}
}
在上面的示例中,我们定义了一个名为 Shape
的接口,该接口要求实现一个 Area
方法,用于计算图形的面积。然后,我们定义了两个结构体 Circle
和 Rectangle
,并分别实现了 Area
方法。通过使用接口,我们可以将不同类型的图形对象放入同一个切片中,然后通过循环调用它们的 Area
方法。
接口的实现
任何类型只要实现了接口中定义的所有方法,就被认为是实现了该接口。接口的实现是隐式的,不需要显式声明。只要方法的签名和接口中的方法签名相同,类型就被视为实现了接口。
接口的多态性
由于接口的多态性,我们可以将实现了接口的对象视为接口本身。在上面的示例中,shapes
切片中存储了不同类型的对象,但它们都实现了 Shape
接口,因此可以通过统一的方式调用 Area
方法。
通过使用接口,可以实现代码的抽象和解耦,使得代码更加灵活和可扩展。接口在Go语言中被广泛应用,用于定义通用的行为和约束。
5.扩展和复用
在Go语言中,扩展和复用代码的方式与传统的面向对象语言(如Java)有所不同。Go鼓励使用组合、接口和匿名字段等特性来实现代码的扩展和复用,而不是通过类继承。
创建extend目录用于后续练习,以下是关于Go语言中扩展和复用的详细讲解:
组合和嵌套
Go语言中的组合(composition)允许你将一个结构体类型嵌套在另一个结构体类型中,从而实现代码的复用。嵌套的结构体可以通过字段名直接访问其成员。创建composition_test.go
package extendimport ("fmt""testing"
)type Engine struct {Model string
}type Car struct {EngineBrand string
}func TestComposition(t *testing.T) {car := Car{Engine: Engine{Model: "V6"},Brand: "Toyota",}fmt.Println("Car brand:", car.Brand)fmt.Println("Car engine model:", car.Model) // 直接访问嵌套结构体的字段
}
在这个示例中,我们使用了组合来创建 Car
结构体,其中嵌套了 Engine
结构体。通过嵌套,Car
结构体可以直接访问 Engine
结构体的字段。
接口实现
通过接口,可以定义一组方法,然后不同的类型可以实现这些方法。这样可以实现多态性和代码解耦,使得不同类型的对象可以通过相同的接口进行操作。创建interface_ext_test.go
package extendimport ("fmt""math""testing"
)// 定义 Shape 接口
type Shape interface {Area() float64Perimeter() float64
}// 定义 Circle 结构体
type Circle struct {Radius float64
}// 实现 Circle 结构体的方法,以满足 Shape 接口
func (c Circle) Area() float64 {return math.Pi * c.Radius * c.Radius
}func (c Circle) Perimeter() float64 {return 2 * math.Pi * c.Radius
}// 定义 Rectangle 结构体
type Rectangle struct {Width float64Height float64
}// 实现 Rectangle 结构体的方法,以满足 Shape 接口
func (r Rectangle) Area() float64 {return r.Width * r.Height
}func (r Rectangle) Perimeter() float64 {return 2 * (r.Width + r.Height)
}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("Shape Type: %T\n", shape)fmt.Printf("Area: %.2f\n", shape.Area())fmt.Printf("Perimeter: %.2f\n", shape.Perimeter())fmt.Println("------------")}
}
在上述示例中,我们定义了一个名为 Shape
的接口,它有两个方法 Area()
和 Perimeter()
,分别用于计算形状的面积和周长。然后,我们分别实现了 Circle
和 Rectangle
结构体的这两个方法,使它们满足了 Shape
接口。
通过将不同类型的形状实例放入一个 []Shape
切片中,我们可以使用统一的方式调用 Area()
和 Perimeter()
方法,实现了代码的多态性和解耦。这样,无论我们后续添加新的形状,只要它们实现了 Shape
接口的方法,就可以无缝地集成到计算器中。
匿名字段和方法重用
通过使用匿名字段,一个结构体可以继承另一个结构体的字段和方法。创建other_ext_test.go
package extendimport ("fmt""testing"
)type Animal struct {Name string
}func (a Animal) Speak() {fmt.Println("Animal speaks")
}type Dog struct {AnimalBreed string
}func TestOtherExt(t *testing.T) {dog := Dog{Animal: Animal{Name: "Buddy"},Breed: "Golden Retriever",}fmt.Println("Dog name:", dog.Name)dog.Speak() // 继承了 Animal 的 Speak 方法
}
在上述示例中,Dog
结构体嵌套了 Animal
结构体,从而继承了 Animal
的字段和方法。
通过这些方式,你可以在Go语言中实现代码的扩展和复用。尽管Go不像传统的面向对象语言那样强调类继承,但通过组合、接口和匿名字段等特性,你仍然可以实现类似的效果,使代码更灵活、可读性更高,并保持低耦合性。
6.空接口和断言
空接口和断言是Go语言中用于处理不确定类型和类型转换的重要概念。
创建emptyassert目录用于后续练习,下面是关于空接口和断言的学习总结:
空接口(Empty Interface)
空接口是Go语言中最基础的接口,它不包含任何方法声明。因此,空接口可以用来表示任何类型的值。空接口的声明方式为 interface{}
。
空接口的主要用途是在需要处理不确定类型的场景中。通过使用空接口,可以接受和存储任何类型的值,类似于其他编程语言中的动态类型。但需要注意的是,使用空接口可能会导致类型安全性降低,因为编译时无法检查具体类型。
断言(Type Assertion)
断言是一种在空接口中恢复具体类型的机制,它允许我们在运行时检查空接口中的值的实际类型,并将其转换为相应的类型。断言的语法为 value.(Type)
,其中 value
是接口值,Type
是要断言的具体类型。
创建emptyassert_test.go进行验证:
package emptyassertimport ("fmt""testing"
)func DoSomething(p interface{}) {switch v := p.(type) {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"str, ok := x.(string)if ok {fmt.Println("String:", str)} else {fmt.Println("Not a string")}
}
下面逐个解释每个测试函数的内容:
-
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语言中空接口的断言操作,通过类型断言可以判断空接口中的具体类型,并执行相应的操作。
总结: 空接口和断言是Go语言中处理不确定类型和类型转换的强大工具。空接口允许存储任何类型的值,而断言允许我们在运行时检查和转换接口值的实际类型。使用这些机制,可以在需要处理不同类型的值时实现更灵活和通用的代码。但在使用空接口和断言时,要注意维护类型安全性,并进行适当的错误处理。
7.GO 接口最佳实践
在Go语言中,使用接口的最佳实践可以提高代码的可读性、可维护性和灵活性。
- 小接口与大接口: 尽量设计小接口,一个接口应该只包含少量的方法,而不是设计一个大而全的接口。这样可以避免实现接口时不必要的负担,并使接口更具通用性。
- 基于使用场景设计接口: 设计接口时应该考虑使用场景,而不是从具体的实现出发。思考在你的应用程序中如何使用接口,以及接口应该提供哪些方法来满足这些使用场景。
- 使用合适的命名: 为接口和方法使用清晰的命名,使其能够表达出其用途和功能。命名应该具有可读性和表达性,让其他开发者能够轻松理解接口的用途。
- 避免不必要的接口: 不要为每个类型都创建一个接口,只有在多个类型之间确实存在共享的行为和功能时才使用接口。不要过度使用接口,以免导致不必要的复杂性。
- 使用接口作为函数参数和返回值: 使用接口作为函数参数和返回值,可以使函数更加通用,允许传入不同类型的参数,并返回不同类型的结果。这可以提高代码的复用性和扩展性。
- 注释和文档: 为接口提供清晰的文档和注释,说明接口的用途、方法的功能和预期行为。这可以帮助其他开发者更好地理解接口的使用方式。
- 用例驱动设计: 在设计接口时,可以从使用的角度出发,先考虑接口在实际场景中如何被调用,然后再设计接口的方法和签名。
- 将接口的实现与定义分离: 将接口的实现与接口的定义分开,这样可以使实现更灵活,可以在不修改接口定义的情况下实现新的类型。
- 默认实现: 在接口定义中,可以为某些方法提供默认实现,从而减少实现接口时的工作量。这对于可选方法或者某些方法的默认行为很有用。
- 使用空接口谨慎: 使用空接口(
interface{}
)应谨慎,因为它会降低类型安全性。只有在确实需要处理不同类型的值时才使用空接口,同时要注意类型断言和错误处理。
设计和使用接口时要根据实际需求和项目的特点来选择合适的方案。
(六)编写好错误机制
src目录下创建chapter6,Go语言中的错误处理机制是通过返回错误值来实现的,而不是使用异常。这种错误处理机制非常清晰、可控,使得开发者能够精确地处理各种错误情况。
1.基本使用介绍
创建basic目录,编写basic_error_test.go
错误类型
在Go中,错误被表示为一个实现了 error
接口的类型。error
接口只有一个方法,即 Error() string
,它返回一个描述错误的字符串。
type error interface {Error() string
}
返回错误值
当一个函数遇到错误情况时,通常会返回一个错误值。这个错误值可以是一个实现了 error
接口的自定义类型,也可以是Go标准库中预定义的错误类型,如 errors.New()
创建的错误。
错误检查
调用者通常需要显式地检查函数返回的错误,以判断是否发生了错误。这可以通过在调用函数后使用 if
语句来实现。
以上两个直接写代码如下:
package basicimport ("errors""fmt""testing"
)var LessThanTwoError = errors.New("n should be not less than 2")
var LargerThenHundredError = errors.New("n should be not larger than 100")func GetFibonacci(n int) ([]int, error) {if n < 2 {return nil, LessThanTwoError}if n > 100 {return nil, LargerThenHundredError}fibList := []int{1, 1}for i := 2; /*短变量声明 := */ i < n; i++ {fibList = append(fibList, fibList[i-2]+fibList[i-1])}return fibList, nil
}func TestGetFibonacci(t *testing.T) {if v, err := GetFibonacci(1); err != nil {if err == LessThanTwoError {fmt.Println("It is less.")}t.Error(err)} else {t.Log(v)}}
2.错误链
创建chain目录,编写error_chain_test.go
在某些情况下,错误可以包含附加信息,以便更好地理解错误的原因。可以通过 fmt.Errorf()
函数来创建包含附加信息的错误。
假设我们正在构建一个文件操作的库,其中包含文件读取和写入功能。有时,在文件读取或写入过程中可能会出现各种错误,例如文件不存在、权限问题等。我们希望能够提供有关错误的更多上下文信息。
package chainimport ("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)
}// 模拟文件读取操作
func ReadFile(path string) ([]byte, error) {// 模拟文件不存在的情况return nil, &FileError{Op: "read", Path: path, Err: errors.New("file not found")}
}func TestChain(t *testing.T) {filePath := "/path/to/nonexistent/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)}}
}
下面是代码的解释:
-
FileError
结构体:定义了一个自定义错误类型FileError
,包含以下字段:Op
:操作类型,表示是读取("read")还是写入("write")操作;Path
:文件路径,表示涉及哪个文件;Err
:原始错误,包含底层的错误信息。 -
Error()
方法:为FileError
结构体实现了error
接口的Error()
方法,用于生成错误的文本描述。 -
ReadFile()
函数:模拟文件读取操作。在这个示例中,该函数返回一个FileError
类型的错误,模拟了文件不存在的情况。 -
定义了一个文件路径TestChain()
测试函数:演示如何在错误处理中使用自定义错误类型。filePath
,并调用ReadFile(filePath)
函数来模拟文件读取操作;检查错误,如果发生错误,输出错误信息;在错误处理中,通过类型断言检查错误是否为*FileError
类型,如果是,则可以提取更多上下文信息,如操作类型、文件路径和原始错误信息。
3.Panic 和 Recover
在Go语言中,panic
和 recover
是用于处理异常情况的机制,但它们应该谨慎使用,仅用于特定的情况,而不是替代正常的错误处理机制。以下是对 panic
和 recover
的详细解释,并给出一个具体用例:
panic
创建panic
目录,编写panic
_test.go。panic
是一个内置函数,用于引发运行时恐慌。当程序遇到无法继续执行的致命错误时,可以使用 panic
来中断程序的正常流程。但应该避免滥用 panic
,因为它会导致程序崩溃,不会提供友好的错误信息。典型情况下,panic
用于表示程序中的不可恢复错误,例如切片索引越界。
package panicimport ("fmt""testing"
)func TestPanic(t *testing.T) {arr := []int{1, 2, 3}index := 4if index >= len(arr) {panic("Index out of range")}element := arr[index]fmt.Println("Element:", element)
}
在上述示例中,如果索引 index
超出了切片 arr
的范围,会触发 panic
,导致程序崩溃。这种情况下,panic
用于表示程序的不可恢复错误。
recover
创建recover
目录,编写recover
_test.go。recover
也是一个内置函数,用于恢复 panic
引发的运行时恐慌。它只能在延迟函数(defer
)内部使用,并且用于恢复程序的控制流,而不是用于处理错误。通常,在发生 panic
后,recover
可以在延迟函数中捕获 panic
,并执行一些清理工作,然后程序会继续执行。
package recoverimport ("fmt""testing"
)func cleanup() {if r := recover(); r != nil {fmt.Println("Recovered from panic:", r)}
}func TestRecover(t *testing.T) {defer cleanup()panic("Something went wrong")fmt.Println("This line will not be executed")
}
在上述示例中,panic
触发后,cleanup
函数中的 recover
捕获了 panic
,并打印了错误消息。然后程序会继续执行,但需要注意的是,控制流不会回到触发 panic
的地方,因此 fmt.Println
不会被执行。
总之,panic
和 recover
应该谨慎使用,只用于特殊情况,如不可恢复的错误或在延迟函数中进行清理操作。在大多数情况下,应该优先使用错误返回值来处理异常情况,因为这种方式更安全、可控,能够提供更好的错误信息和错误处理。只有在特定的情况下,例如遇到不可恢复的错误时,才应该考虑使用 panic
和 recover
。
4.自定义错误类型
创建define
目录,编写error_define
_test.go。
在Go中,你可以根据需要定义自己的错误类型,只需满足 error
接口的要求即可。这允许你创建更具描述性和上下文的错误类型。
在Go中,自定义错误类型是一种强大的方式,可以创建更具描述性和上下文的错误,以提供更好的错误信息。自定义错误类型必须满足 error
接口的要求,即实现 Error() string
方法。以下是一个示例,展示如何自定义错误类型和验证其用例:
package defineimport ("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("2006-01-02 15:04:05"))
}// 模拟执行某个操作,可能会超时
func PerformOperation() error {// 模拟操作超时timeout := time.Now().Add(5 * time.Second)if time.Now().After(timeout) {return TimeoutError{Operation: "PerformOperation", Timeout: timeout}}// 模拟操作成功return nil
}func TestDefineError(t *testing.T) {err := PerformOperation()if err != nil {// 检查错误类型并打印错误信息if timeoutErr, ok := err.(TimeoutError); ok {fmt.Println("Error Type:", timeoutErr.Operation)fmt.Println("Timeout At:", timeoutErr.Timeout)}fmt.Println("Error:", err)} else {fmt.Println("Operation completed successfully.")}
}
下面是代码的解释:
-
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 版本中成为默认的依赖管理方式。
1.package
(包)的基本知识点
基本复用模块单元
在 Go 语言中,package
是代码复用的基本单元。一个 package
可以包含多个 Go 源文件,这些文件可以共享同一个包中的代码,并通过包的导入机制被其他包使用。
包的可见性:在 Go 语言中,通过首字母大写来表明一个标识符(如变量、函数、类型等)可以被包外的代码访问。反之,首字母小写的标识符只能在包内使用。
// mypackage.go
package mypackage// 公有函数,其他包可以访问
func PublicFunction() {// 实现细节
}// 私有函数,仅在当前包内可访问
func privateFunction() {// 实现细节
}
代码的 package
可以和所在的目录不一致
Go 语言的文件组织结构鼓励但不强制 package
名称与其所在目录名称一致。通常情况下,开发者会遵循这种约定以保持代码的一致性和可读性,但 Go 并不强制执行这一规则。
实际应用:你可以在 chapter7 目录下创建多个文件,并在这些文件中定义相同的包名 mypackage
,也可以选择一个不同于目录名的包名。
// src目录下的代码
// src/chapter7/utility.go
package utility // 包名与所在目录名不同func UtilityFunction() {// 实现细节
}
同一目录里的 Go 代码的 package
要保持一致
在同一目录中的所有 Go 文件必须声明相同的 package
名称。这是 Go 语言的一个基本规则,确保同一目录下的所有文件都属于同一个包,从而能够互相访问这些文件中声明的标识符。
违例情况:如果你在同一目录下使用不同的 package
名称,Go 编译器将会报错,提示包声明不一致。这个在上面的案例中也可以直接看到。
2.构建一个自身可复用的package
src目录下创建chapter7后,再次新建series,编写my_series.go如下:
package seriesimport "fmt"func init() {fmt.Println("init1")
}func init() {fmt.Println("init2")
}func Square(n int) int {return n * n
}func GetFibonacciSerie(n int) []int {ret := []int{1, 1}for i := 2; i < n; i++ {ret = append(ret, ret[i-2]+ret[i-1])}return ret
}
然后在chapter7中新建client,编写package_test.go将上面的内容引入:
package clientimport ("go-learning/src/chapter7/series""testing"
)func TestPackage(t *testing.T) {t.Log(series.GetFibonacciSerie(5))t.Log(series.Square(5))
}
通过在 chapter7
目录下创建一个名为 series
的包,把与数学相关的函数(如求平方和斐波那契数列)集中在一起。这样在其他地方需要使用这些功能时,只需引入这个包即可,不必重复编写相同的代码。
知识点:包的初始化
- 利用 Go 语言中的
init()
函数机制进行包的初始化操作。在 Go 中,每个包可以有多个init()
函数,这些函数会在包第一次被加载时自动执行,且执行顺序按照代码顺序。 - 在
series
包中编写了两个init()
函数,它们会在包被引入时自动执行。这种机制可以用于在包加载时执行一些必要的初始化工作(如设置默认值、加载配置等),或者用来调试包的加载过程。
3.导入和应用远程依赖(即外部包)
获取和更新远程依赖
- 使用
go get
命令来下载并添加远程依赖到项目中。Go Modules 会自动管理这些依赖,并更新go.mod
和go.sum
文件。 - 如果需要强制从网络获取最新版本的依赖,可以使用
-u
参数:- 示例:
go get -u github.com/user/repo
这将更新指定包及其依赖项到最新的次要版本或修订版本。
- 示例:
代码在 GitHub 上的组织形式
- 确保代码库的目录结构直接反映包的导入路径,而不要使用
src
目录作为根目录。这使得项目更容易与 Go 的依赖管理工具兼容,确保导入路径的简洁和一致性。 -
最佳实践:在 GitHub 上组织代码时,目录结构应与包名匹配,例如:github.com/username/project/ ├── mypackage/ │ └── mypackage.go └── anotherpackage/└── anotherpackage.go
- 这样可以避免导入路径中的多余层级,并确保使用
go get
时能正确定位包。
按照该思路我们进行验证,在在 chapter7
目录下创建一个名为 remote_package
的包,我们先进行下载“go get github.com/easierway/concurrent_map”的下载,然后创建remote_package_test.go进行验证:
package remoteimport ("fmt""testing"cm "github.com/easierway/concurrent_map"
)func TestConcurrentMap(t *testing.T) {m := cm.CreateConcurrentMap(99)m.Set(cm.StrKey("key"), 10)value, ok := m.Get(cm.StrKey("key"))if ok {fmt.Println("Key found:", value)t.Log(m.Get(cm.StrKey("key")))}
}
concurrent_map的介绍:concurrent_map
是一个由 GitHub 用户 easierway
创建的 Go 包,主要用于实现线程安全的并发 map 数据结构。这个包提供了一种简单且高效的方式来处理并发环境下的 map 操作,避免了传统 map 在多 goroutine 访问时出现的竞争问题。
功能/特点 | 说明 |
---|---|
线程安全 | 通过分段锁机制(分片锁)确保 map 在多 goroutine 并发访问时的数据安全。 |
高效的读写操作 | 将 map 分成多个子 map,减少锁的粒度,提高并发访问的效率。 |
简单易用的 API | 提供类似标准 map 的接口,如 Set 、Get 、Remove ,使用方式简单。 |
动态扩展 | 根据使用需求动态扩展或收缩分段,提高资源利用率。 |
4.包的依赖管理
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.mod
和go.sum
文件管理版本依赖,确保项目构建过程中使用的依赖版本是可预测且稳定的。即使某个依赖包发布了新版本,你的项目仍会使用go.mod
中指定的版本,除非你主动升级。
虽然 Go Modules 解决了许多依赖管理问题,但它也带来了一些新的挑战:
- 多模块项目的管理:在一些大型项目中,可能会有多个模块,这些模块之间的依赖管理需要谨慎处理,特别是当这些模块之间存在依赖关系时。
- 依赖冲突:如果不同的依赖项依赖于同一个包的不同版本,Go Modules 会尝试找到一个可用的共同版本,但这可能并不总是理想的解决方案。
Go Modules 通过模块化和版本控制,基本解决了 Go 语言早期依赖管理中的主要问题,如同一环境下不同项目使用同一包的不同版本,以及对包的特定版本的依赖管理问题。然而,尽管如此,随着项目规模的扩大和依赖关系的复杂化,依赖管理仍然需要开发者谨慎对待。
(八)并发编程
src目录下创建chapter8,展开后续的学习。
1.协程机制
Thread vs Goroutine
Java 的线程(Thread)与 Go 语言的协程(Goroutine)在设计哲学和实现细节上有很大的不同,主要表现在栈大小及与内核空间实体(KSE)的对应关系方面:
比较项 | Java Thread | Goroutine |
---|---|---|
栈的初始大小 | 1MB(JDK5 及以后版本) | 2KB |
栈的增长方式 | 固定大小,超出时抛出 StackOverflowError | 动态增长,最大可扩展到 1GB |
与内核线程的对应关系 | 1:1 模型,每个 Java 线程对应一个内核线程 | M模型,多个 Goroutine 对应少量内核线程 |
调度方式 | 由操作系统调度 | 由 Go 运行时调度 |
创建和调度的开销 | 较大,创建和切换线程的开销较高 | 较小,创建和调度 Goroutine 的开销非常低 |
并发处理能力 | 创建大量线程时可能影响系统性能 | 可以高效创建和管理大量 Goroutine |
Goroutine 的调度原理
左侧图示展示了 Goroutine 在正常调度情况下的工作原理:
-
M(System Thread):代表操作系统的线程,图中有两个系统线程,
M0 正在执行某个 Goroutine。M0
和M1
。 -
P(Processor):代表处理器,这里是 Go 的调度器中的一个抽象概念,而不是实际的 CPU 核心。P 负责执行 Goroutine 的队列,并将它们映射到系统线程(M)上。
图中有一个P
,它将 Goroutine 分配给 M 进行执行。 -
G(Goroutine):代表 Goroutine,图中
G0
,G1
,G2
等分别表示不同的 Goroutine。G0
正在被M0
执行。G1
,G2
仍在等待被调度执行。
右侧图示展示了 Goroutine 发生系统调用(Syscall)时的工作原理:
-
Syscall:当 Goroutine 需要执行系统调用时,执行该 Goroutine 的系统线程会被阻塞。
这里M0
在处理G0
的系统调用,因此M0
被阻塞在系统调用中。 -
M1:系统线程
M1
被调度来继续处理P
中的其他 Goroutine。P
调度了其他的 Goroutine(如G1
,G2
)到新的系统线程M1
上继续执行,从而避免了因为一个 Goroutine 阻塞而导致整个线程阻塞的情况。
调度机制总结
- Go 运行时调度器通过
M:P:G
模型实现了 Goroutine 的高效调度。 M
(系统线程)可以执行多个G
(Goroutine),而P
(Processor)则决定哪些Goroutine
应该运行在M
上。- 当一个 Goroutine 被阻塞时(如执行系统调用),Go 运行时会将该系统线程从调度队列中移除,并将剩余的 Goroutine 调度到其他空闲的系统线程上继续执行。
- 这样可以有效地利用系统资源,避免线程阻塞导致的资源浪费,体现了 Goroutine 的轻量化和高效性。
这张图很直观地展示了 Go 语言中 Goroutine 的 M 模型,如何通过 M
, P
, G
之间的协作,实现高效的并发调度。
直接的代码展示
直接在chapter8下新建groutine,编写groutine_test.go代码如下:
package groutineimport ("fmt""testing""time"
)func sayHello() {fmt.Println("Hello, Goroutine!")
}func TestGroutine(t *testing.T) {for i := 0; i < 10; i++ {go func(i int) {//time.Sleep(time.Second * 1)fmt.Println(i)}(i)}time.Sleep(time.Millisecond * 50)
}func TestSayHello(t *testing.T) {go sayHello() // 启动一个新的 Goroutine 执行 sayHello 函数// 主函数等待一段时间,确保 Goroutine 有机会执行time.Sleep(time.Millisecond * 10)fmt.Println("Main function finished")
}
TestGroutine
展示了如何在循环中创建多个 Goroutine 并并发执行任务,同时说明了 Goroutine 的变量捕获问题。TestSayHello
展示了如何使用 Goroutine 并发执行一个简单的函数,并突出 Goroutine 的非阻塞特性。
通过这两个函数,可以更好地理解 Go 语言中 Goroutine 的基本使用方式以及它们在并发编程中的作用。
2.共享内存并发机制
对的