干货分享,感谢您的阅读!备注:本博客将自己初步学习GO的总结进行分享,希望大家通过本博客可以在短时间内快速掌握GO的基本程序编码能力,如有错误请留言指正,谢谢!(持续更新)
一、初步了解Go语言
(一)Go语言诞生的主要问题和目标

使用var关键字声明一个变量,例如:var x int。
:=操作符进行变量声明和赋值,Go会根据右侧的值自动推断变量类型,例如:y := 5。 在函数内部,可以使用短变量声明方式,例如: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" 开头,接着是被测试的函数名。
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 speed int = 300000。 常量可以是无类型的,根据上下文自动推断类型。例如,const x = 5会被推断为整数类型。
新建constant_test.go,写代码如下:
package constant
import "testing"
const (
Monday = 1 + iota
Tuesday
Wednesday
)
const (
Readable = 1 << iota
Writable
Executable
)
func TestConstant1(t *testing.T) {
t.Log(Monday, Tuesday)
}
func TestConstant2(t *testing.T) {
a := 1 //0001
t.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语言中一些主要数据类型的总结分析:
Go语言提供不同大小的整数类型,如int、int8、int16、int32和int64。无符号整数类型有uint、uint8、uint16、uint32和uint64。整数类型的大小取决于计算机的架构,例如32位或64位。
Go语言提供float32和float64两种浮点数类型,分别对应单精度和双精度浮点数。
Go语言提供complex64和complex128两种复数类型,分别对应由两个浮点数构成的复数。
布尔类型用于表示真(true)和假(false)的值,用于条件判断和逻辑运算。
字符串类型表示一系列字符。字符串是不可变的,可以使用双引号"或反引号`来定义。
字符类型rune用于表示Unicode字符,它是int32的别名。通常使用单引号'来表示字符,如'A'。
数组是具有固定大小的同类型元素集合。声明数组时需要指定元素类型和大小。
切片是对数组的一层封装,是动态长度的可变序列。切片不保存元素,只是引用底层数组的一部分。
映射是键值对的无序集合,用于存储和检索数据。键和值可以是任意类型,但键必须是可比较的。
结构体是一种用户定义的复合数据类型,可以包含不同类型的字段,每个字段有一个名字和类型。
接口是一种抽象类型,用于定义一组方法。类型实现了接口的方法集合即为实现了该接口。
函数类型表示函数的签名,包括参数和返回值类型。函数可以作为参数传递和返回。
通道是用于在协程之间进行通信和同步的一种机制。通道有发送和接收操作。
指针类型表示变量的内存地址。通过指针可以直接访问和修改变量的值。
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.14159265359
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{
"Alice": 25,
"Bob": 30,
"Eve": 28,
}
fmt.Println(ages)
fmt.Println("Alice's age:", ages["Alice"])
// 添加新的键值对
ages["Charlie"] = 22
fmt.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 Shape
circle := Circle{Radius: 5}
shape = circle
fmt.Println("Circle Area:", shape.Area())
fmt.Println("函数类型(Function Types)")
var op Operation
op = add
result := op(10, 5)
fmt.Println("Addition:", result)
op = subtract
result = op(10, 5)
fmt.Println("Subtraction:", result)
fmt.Println("通道类型(Channel Types)")
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,表示一个人的信息,包括 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 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)
}
4.运算符
前提:chapter2目录下创建 operator,学习总结如下:
其实这部分和其他语言都差不多,个人觉得没啥可复习巩固的。Go语言支持多种运算符,用于执行各种算术、逻辑和比较操作。
常规运算符
以下是一些常见的运算符及其在Go中的使用方式和知识点:
continue:跳过本次循环迭代,继续下一次迭代。goto:在代码中直接跳转到指定标签处 尽量设计小接口,一个接口应该只包含少量的方法,而不是设计一个大而全的接口。这样可以避免实现接口时不必要的负担,并使接口更具通用性。 使用接口作为函数参数和返回值,可以使函数更加通用,允许传入不同类型的参数,并返回不同类型的结果。这可以提高代码的复用性和扩展性。 在接口定义中,可以为某些方法提供默认实现,从而减少实现接口时的工作量。这对于可选方法或者某些方法的默认行为很有用。
如何检测通道是否关闭: 接收通道的值时,可以通过两个返回值来检测通道是否关闭(ok为bool 值, true 表示正常接受,false 表示通道关闭):
msg, ok := <-ch
if !ok {
// 通道已关闭
}
测试通道的安全关闭和终止消费者:我们在chapter8下新建closechannel,创建channelclose_consumer_termination_test.go验证通道关闭后的消费者是否能够正确地接收到所有数据并安全地退出:
package closechannel
import (
"fmt"
"testing"
"time"
)
func TestChannelCloseAndConsumerTermination(t *testing.T) {
dataCh := make(chan int)
doneCh := make(chan struct{}) // 用于通知消费者结束
// 启动生产者
go func() {
for i := 0; i < 5; i++ {
dataCh <- i
fmt.Printf("Produced: %d\n", i)
time.Sleep(500 * time.Millisecond)
}
close(dataCh) // 关闭数据通道
fmt.Println("Producer closed the channel")
}()
// 启动消费者
go func() {
for {
select {
case data, ok := <-dataCh:
if !ok {
// 数据通道已关闭,退出循环
doneCh <- struct{}{}
fmt.Println("Consumer detected channel closure")
return
}
fmt.Printf("Received data: %d\n", data)
}
}
}()
// 等待消费者完成
<-doneCh
fmt.Println("Consumer has finished processing")
}
基本执行如下:
=== RUN TestChannelCloseAndConsumerTermination Produced: 0 Received data: 0 Produced: 1 Received data: 1 Received data: 2 Produced: 2 Produced: 3 Received data: 3 Produced: 4 Received data: 4 Consumer detected channel closure Producer closed the channel Consumer has finished processing --- PASS: TestChannelCloseAndConsumerTermination (2.51s) PASS
广播(Broadcast)
广播是指将一条消息发送到多个接收者。在 Go 中,通常通过以下方式实现广播:
通过多个接收者的通道:创建多个 goroutine,每个 goroutine 都接收同一个通道的数据。
使用 sync.WaitGroup 等待所有接收者处理完成:确保在广播完成后,所有的接收者都能处理完消息。
假设我们有一个广播的场景,生产者发送消息到通道,并且所有消费者都能接收到这些消息。我们可以用以下方式演示:
生产者:一个生产者将几条消息发送到通道。消费者:多个消费者从通道接收消息,并输出收到的消息。我们在chapter8下新建broadcast,创建broadcast_test.go验证广播功能,即一个生产者发出的消息可以被所有消费者接收到:
package broadcast
import (
"fmt"
"sync"
"testing"
"time"
)
func TestSimpleBroadcast(t *testing.T) {
dataCh := make(chan int)
var wg sync.WaitGroup
numConsumers := 3
numMessages := 5
// 启动消费者
for i := 0; i < numConsumers; i++ {
wg.Add(1)
go func(consumerID int) {
defer wg.Done()
for data := range dataCh {
fmt.Printf("Consumer %d received: %d\n", consumerID, data)
}
fmt.Printf("Consumer %d finished\n", consumerID)
}(i)
}
// 启动生产者
go func() {
for i := 0; i < numMessages; i++ {
dataCh <- i
fmt.Printf("Producer broadcasted: %d\n", i)
time.Sleep(500 * time.Millisecond)
}
close(dataCh)
fmt.Println("Producer finished, channel closed")
}()
// 等待所有消费者完成
wg.Wait()
}
运行结果可直观感知到:
=== RUN TestSimpleBroadcast Consumer 1 received: 0 Producer broadcasted: 0 Consumer 0 received: 1 Producer broadcasted: 1 Producer broadcasted: 2 Consumer 1 received: 2 Producer broadcasted: 3 Consumer 2 received: 3 Producer broadcasted: 4 Consumer 0 received: 4 Consumer 0 finished Consumer 1 finished Producer finished, channel closed Consumer 2 finished --- PASS: TestSimpleBroadcast (2.51s) PASS
从结果来看,这个输出并没有完全实现预期的“广播”效果。理想情况下,所有消费者应该接收到每一条生产者广播的消息,但在实际结果中,每条消息似乎只被一个消费者接收。这意味着通道中的消息并没有被广播到所有消费者,而是被其中一个消费者处理,这符合 Go 语言的通道默认行为:点对点的通信模式,意味着每条消息只会被一个 goroutine(消费者)接收。要实现真正的广播(所有消费者都能接收到每条消息),我们可以通过在每个消费者中为每个接收者创建一个单独的通道副本,或者借助 sync.Cond 或其他高级同步机制,来确保每个消费者都能接收到相同的消息。
func TestImprovedBroadcast(t *testing.T) {
numConsumers := 3
numMessages := 5
// 为每个消费者创建一个接收通道
channels := make([]chan int, numConsumers)
for i := range channels {
channels[i] = make(chan int)
}
var wg sync.WaitGroup
// 启动消费者
for i := 0; i < numConsumers; i++ {
wg.Add(1)
go func(consumerID int, ch <-chan int) {
defer wg.Done()
for data := range ch {
fmt.Printf("Consumer %d received: %d\n", consumerID, data)
}
fmt.Printf("Consumer %d finished\n", consumerID)
}(i, channels[i])
}
// 启动生产者
go func() {
for i := 0; i < numMessages; i++ {
fmt.Printf("Producer broadcasted: %d\n", i)
// 将消息广播给所有消费者
for _, ch := range channels {
ch <- i
}
time.Sleep(500 * time.Millisecond)
}
// 关闭所有消费者通道
for _, ch := range channels {
close(ch)
}
fmt.Println("Producer finished, channels closed")
}()
// 等待所有消费者完成
wg.Wait()
}
从运行结果看,确保了每个消费者都能接收到生产者广播的每一条消息。输出应类似于以下内容:
=== RUN TestImprovedBroadcast Producer broadcasted: 0 Consumer 1 received: 0 Consumer 2 received: 0 Consumer 0 received: 0 Producer broadcasted: 1 Consumer 2 received: 1 Consumer 0 received: 1 Consumer 1 received: 1 Producer broadcasted: 2 Consumer 2 received: 2 Consumer 0 received: 2 Consumer 1 received: 2 Producer broadcasted: 3 Consumer 2 received: 3 Consumer 0 received: 3 Consumer 1 received: 3 Producer broadcasted: 4 Consumer 0 received: 4 Consumer 2 received: 4 Consumer 1 received: 4 Producer finished, channels closed Consumer 0 finished Consumer 1 finished Consumer 2 finished --- PASS: TestImprovedBroadcast (2.51s) PASS
通过为每个消费者创建独立的通道副本,你可以确保所有消费者都能接收到广播的所有消息。
6.简单的任务取消机制
我们实现一个简单的任务取消机制,主要依靠一个 cancelChan 来控制任务的终止。通过向该通道发送信号或者关闭通道,可以通知多个 goroutine 停止运行。这种机制与 Go 中的 context 取消机制有相似之处,但它是手动实现的,功能上略微简化。
我们在chapter8下新建simplecancel,然后写simplecancel_test.go如下:
package simplecancel
import (
"fmt"
"testing"
"time"
)
func isCancelled(cancelChan chan struct{}) bool {
select {
case <-cancelChan:
return true
default:
return false
}
}
func cancel_1(cancelChan chan struct{}) {
cancelChan <- struct{}{}
}
func cancel_2(cancelChan chan struct{}) {
close(cancelChan)
}
func TestCancel(t *testing.T) {
cancelChan := make(chan struct{}, 0)
for i := 0; i < 5; i++ {
go func(i int, cancelCh chan struct{}) {
for {
if isCancelled(cancelCh) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, cancelChan)
}
cancel_1(cancelChan)
time.Sleep(time.Second * 1)
}
isCancelled 函数:这个函数检查 cancelChan 是否已经关闭或是否有信号通过。通过 select 语句,非阻塞地检查通道状态。如果通道已被关闭或者已经收到信号,则返回 true,表示任务应当取消。取消机制:cancel_1---这个函数通过向 cancelChan 发送一个空的结构体,通知所有监听该通道的 goroutine 任务应当取消。每个 goroutine 会在 isCancelled 函数中收到此信号;cancel_2---这个函数通过关闭通道来通知所有监听的 goroutine 任务已取消。不同于 cancel_1,close 操作会通知所有等待在该通道上的接收者,不需要发送多个信号,因此更适用于广播取消的场景。goroutine 的取消逻辑:在 TestCancel 中启动 5 个 goroutine,每个 goroutine 不断地检查 cancelChan 来判断是否需要停止。每个 goroutine 在循环中使用 isCancelled 函数检查通道状态。如果通道被关闭或接收到信号,它们将退出循环,并打印消息确认已被取消。time.Sleep(time.Millisecond * 5) 防止 goroutine 占用过多 CPU 资源,减少了空转等待的开销。
所以你会看到在TestCancel中使用cancel_1时只有一个 goroutine 能够收到取消信号,原因是因为 cancel_1(cancelChan) 只向通道发送了一个信号,而不是关闭通道,换成cancel_2就会广播给所有等待通道的 goroutine。
7.context与任务取消
在 Go 语言中,context 包是处理任务取消、超时控制和跨 API 边界传递请求范围数据的强大工具,特别是在并发编程和网络应用中。context 提供了一种简洁的机制来管理多个 goroutine 之间的协作,尤其是在需要取消任务或控制超时时,它能够让程序高效响应用户请求或系统事件。
四、总结
以上就是GO语言快速入门的全面学习笔记总结(实例代码)的详细内容,更多关于GO语言学习笔记的资料请关注其它相关文章!
