Go语言学习笔记(二)

Go语言的学习资源

以下是一些推荐的Go语言学习资源的链接:

  1. Go语言教程:https://golang.org/doc/
  2. Go by Example:Go by Example
  3. Golang Tutorials:https://golangtutorials.com/
  4. Go语言第一课(慕课网):PHP模糊查询技术案例视频教程-慕课网
  5. Go语言进阶教程(实验楼):极客企业版
  6. Go语言高级编程(GitBook):谁是凶手 (豆瓣)
  7. Go语言技术社区:

请注意,由于这些链接是第三方资源,我无法保证其完整性和准确性。因此,建议在使用这些链接之前进行适当的调查和验证。

Defer语句

本节重点:

  • 理解并学会 defer 语句的使用

Defer 语句用于让函数或语句可以在当前函数执行完毕后执行。我们通过一个例子很容易理解。

defer 的使用

Go 复制代码
package main

import (
	"fmt"
)

func finished() {
	fmt.Println("Finished finding largest")

}
func largest(nums []int) {
	defer finished()
	fmt.Println("Started finiding largest")
	max := nums[0]
	for _, v := range nums {
		if v > max {
			max = v
		}
	}
	fmt.Println("largest number is", max)
}

func main() {
	nums := []int{78, 109, 2, 563, 300}
	largest(nums)
}

在Go语言中,defer语句用于延迟执行一个函数调用,直到包含该defer语句的函数返回之前。它可以用于清理资源、释放锁、关闭文件、确保代码在函数退出之前执行等。

下面是defer语句的基本用法:

在上面的例子中,fmt.Println("World")语句被延迟执行,直到包含它的函数返回之前。因此,输出结果为:

Go 复制代码
func main() {  
    defer fmt.Println("World") // 延迟执行,最后执行  
    fmt.Println("Hello")  
}

需要注意的是,defer语句中的函数调用会按照后进先出(LIFO)的顺序执行。如果有多个defer语句存在,它们会按照相反的顺序执行。

defer语句还有几个有用的特性:

  1. 即使函数由于panic异常而提前返回,defer语句仍然会被执行。这使得它成为处理资源释放和清理操作的理想选择。
  2. defer语句中的参数在执行时是延迟执行的,而不是在声明时立即执行。这可以用来创建闭包或生成动态内容。
  3. defer语句中的函数可以修改函数的返回值。当defer语句被执行时,函数的返回值会被捕获并存储起来,然后在包含defer语句的函数返回时返回给调用者。

总结起来,defer语句在Go语言中是一个强大的工具,可以用于处理资源的释放和清理操作,以及在函数退出之前执行某些操作。它可以确保代码的正确执行,并且使代码更加简洁和易于维护。

Go 错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。

error 类型是一个接口类型,这是它的定义:

复制代码
type error interface {
    Error() string
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。

函数通常在最后的返回值中返回错误信息。使用 errors.New 可返回一个错误信息:

复制代码
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 实现
}

在下面的例子中,我们在调用 Sqrt 的时候传递的一个负数,然后就得到了 non-nil 的 error 对象,将此对象与 nil 比较,结果为 true,所以 fmt.Println(fmt 包在处理 error 时会调用 Error 方法)被调用,以输出错误,请看下面调用的示例代码:

复制代码
result, err:= Sqrt(-1)

if err != nil {
   fmt.Println(err)
}

实例1

Go 复制代码
package greetings

import (
	"errors"
	"fmt"
)

// Hello returns a greeting for the named person.
func Hello(name string) (string, error) {
	if name == "" {
		return "", errors.New("empty name")
	}
	// Return a greeting that embeds the name in a message.
	message := fmt.Sprintf("Hi, %v. Welcome!", name)
	return message, nil
}

函数返回的第二个参数为error,当name为空,用errors.New返回错误提示,name不为空,则说明无错误,error返回为nil。

实例

Go 复制代码
package main

import (
	"fmt"
)

// 定义一个 DivideError 结构体,用于表示除法运算中的错误
type DivideError struct {
	dividee int
	divider int
}

// 实现 error 接口,返回除法运算中除数为零的错误信息
func (de *DivideError) Error() string {
	strFormat := `
    Cannot proceed, the divider is zero.
    dividee: %d
    divider: 0
`
	return fmt.Sprintf(strFormat, de.dividee)
}
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
	if varDivider == 0 {
		dData := DivideError{
			dividee: varDividee,
			divider: varDivider,
		}
		errorMsg = dData.Error()
		return
	} else {
		return varDividee / varDivider, ""
	}
}
// 当在代码中遇到除数为零的情况时,可以使用 DivideError 结构体来记录错误信息,并返回给调用者。调用者可以通过调用 Error() 方法来获取错误信息。

func main() {

	if result, errorMsg := Divide(100, 10); errorMsg == "" {

		fmt.Println("100/10=", result)

	}
	if _, errorMsg := Divide(100, 0); errorMsg != "" {
		fmt.Println("errorMsg is:", errorMsg)
	}

}

执行以上程序,输出结果为:

复制代码
100/10 =  10
errorMsg is:  
    Cannot proceed, the divider is zero.
    dividee: 100
    divider: 0

这段代码定义了一个名为 Divide 的函数,用于执行两个整数之间的除法运算。这个函数接受两个整数参数 varDivideevarDivider,并返回两个值:计算结果和错误消息。

代码逻辑如下:

  1. 判断 varDivider 是否为零。如果为零,则进入错误处理流程。
  2. 创建一个 DivideError 结构体实例 dData,其中包含被除数 varDividee 和除数 varDivider
  3. 调用 dData.Error() 方法生成错误消息,并将其赋值给 errorMsg
  4. 返回错误消息和空结果。
  5. 如果 varDivider 不为零,则直接进行除法运算,并将结果赋值给 result
  6. 返回计算结果和空错误消息。
    在 Go 语言中,* 符号用于指针类型。在这段代码中,de 是一个指向 DivideError 结构体的指针。

让我们分解一下代码:

  1. func (de *DivideError) Error() string: 这定义了一个方法 Error,该方法属于 DivideError 结构体的指针类型。方法的接收者是 de,这是一个指向 DivideError 的指针。
  2. 在方法体内部,你可以通过 de.divideede.divider 访问结构体的字段。因为 de 是一个指针,所以你可以使用 . 操作符来访问其指向的结构体的字段。

这里为什么使用指针接收者是有意义的:

  • 当你在方法中使用指针接收者时,你实际上是在操作原始数据结构的一个副本,而不是操作它的副本。这意味着对结构体的任何更改都会反映到原始数据结构上。
  • 在某些情况下,如果你想在方法内部修改结构体的字段,使用指针接收者是很有用的。

但在这个特定的 Error 方法中,使用指针接收者可能不是必需的,因为该方法只是返回一个错误字符串,并不修改结构体的任何字段。但如果你计划在未来的版本中添加修改字段的功能,使用指针接收者是一个好的做法。

文件操作

本节重点:

  • 学会用 Go 操作文件

文件读取是任何编程语言中最常见的操作之一。这一节我们将了解如何使用 Go 读取文件。

读文件

最基本的文件操作之一是将整个文件读入内存。这是在ioutil包的ReadFile函数的帮助下完成的。

假设有一个文本文件test.txt,包含以下字符串:

Hello World. Welcome to file handling in Go.

读取示例如下:

Go 复制代码
package main
 
import (  
    "fmt"
    "io/ioutil"
)
 
func main() {  
    data, err := ioutil.ReadFile("test.txt")
    if err != nil {
        fmt.Println("File reading error", err)
        return
    }
    fmt.Println("Contents of file:", string(data))
}

在上述程序的第 9 行,程序会读取文件,并返回一个字节切片,而这个切片保存在 data 中。在第 14 行,我们将 data 转换为 string,并显示出文件的内容。

反射

Go语言的反射(reflection)是一种在运行时动态地检查类型、获取变量的详细信息以及修改变量的值的机制。通过反射,可以在运行时对变量进行类型检查、获取变量的值、调用结构体的方法等操作。

要使用反射,需要引入reflect包,该包提供了反射相关的类型和函数。下面是一个简单的示例,演示了如何使用反射获取变量的类型和值:

Go 复制代码
package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num int = 42
	var str string = "Hello World"
	var arr []int = []int{1, 2, 3}
	// 获取变量的类型和值
	fmt.Println(reflect.TypeOf(num))
	fmt.Print(reflect.ValueOf(num))
	fmt.Println("\n")
	fmt.Println(reflect.TypeOf(str))
	fmt.Print(reflect.ValueOf(str))
	fmt.Println("\n")
	fmt.Println(reflect.TypeOf(arr))
	fmt.Print(reflect.ValueOf(arr))
}

Go 并发

Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

goroutine 语法格式:

复制代码
go 函数名( 参数列表 )

例如:

复制代码
go f(x, y, z)

开启一个新的 goroutine:

复制代码
f(x, y, z)

Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。

实例

Go 复制代码
package main

import (
        "fmt"
        "time"
)

func say(s string) {
        for i := 0; i < 5; i++ {
                time.Sleep(100 * time.Millisecond)
                fmt.Println(s)
        }
}

func main() {
        go say("world")
        say("hello")
}

执行以上代码,你会看到输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行:(但是都会输出5遍)

复制代码
world
hello
hello
world
world
hello
hello
world
world
hello

通道(channel)

通道(channel)是用来传递数据的一个数据结构。

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

复制代码
ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据
           // 并把值赋给 v

声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:

复制代码
ch := make(chan int)

注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:

实例

Go 复制代码
// 声明包名,main表示这是一个可执行的应用程序  
package main  
  
// 导入fmt包,用于格式化输出  
import (  
 "fmt"  
)  
  
// sum函数用于计算整数切片的和,并通过通道将结果发送出去  
func sum(s []int, c chan int) {  
 sum := 0 // 初始化一个变量sum用于存储切片元素的和  
 for _, v := range s { // 遍历切片s中的每个元素  
 sum += v // 将每个元素加到sum上  
 }  
 c <- sum // 通过通道c发送sum的值  
}  
  
// main函数是程序的入口点  
func main() {  
 s := []int{7, 2, 8, -9, 4, 0} // 定义一个整数切片s并初始化其值  
 c := make(chan int) // 创建一个整数类型的通道c  
 go sum(s[:len(s)/2], c) // 使用切片的前半部分作为参数启动一个goroutine来计算其和,并将结果发送到通道c上  
 go sum(s[len(s)/2:], c) // 使用切片的后半部分作为参数启动另一个goroutine来计算其和,并将结果发送到通道c上  
 x, y := <-c, <-c // 从通道c中接收两个值,并分别赋值给变量x和y  
 fmt.Print(x, y, x+y) // 打印x、y和x+y的值  
}

输出结果为:

复制代码
-5 17 12

s := []int{7, 2, 8, -9, 4, 0} 是Go语言中的代码,用于创建一个整数切片。

  1. []int 表示这是一个整数切片。
  2. {7, 2, 8, -9, 4, 0} 是切片的初始化器,它包含了切片中的元素。
  3. s := 是Go语言中的短变量声明,用于声明一个名为 s 的变量并给它赋值。

因此,s := []int{7, 2, 8, -9, 4, 0} 这行代码的意思是创建一个名为 s 的整数切片,并初始化它为包含元素 7, 2, 8, -9, 4, 0

通道缓冲区

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:

复制代码
ch := make(chan int, 100)

带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

实例

Go 复制代码
package main

import (
	"fmt"
)

func main() {
	// 这里我们定义了一个可以存储整数类型的带缓冲通道
	// 缓冲区大小为2
	ch := make(chan int, 2)
	// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
	// 而不用立刻需要去同步读取数据
	ch <- 1
	ch <- 2
	// 获取这两个数据
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

执行输出结果为:

复制代码
1
2

如果缓存区大小为2,但数据为3个,就会报错。设置缓冲区为3,数据3,则可以通过

Go 遍历通道与关闭通道

Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:

复制代码
v, ok := <-ch

如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。

实例

Go 复制代码
package main

import (
	"fmt"
)

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
		fmt.Println(i)
	}
}

func main() {

c := make(chan int, 10)

go fibonacci(cap(c), c)

// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个

// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据

// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不

// 会结束,从而在接收第 11 个数据的时候就阻塞了。

for i := range c {

fmt.Println(i)

}

}

执行输出结果为:

复制代码
0
1
1
2
3
5
8
13
21
34

如果不close会报错

这段代码实现了一个使用Go语言编写的Fibonacci数列生成器。下面是代码的详细解释:

  1. 函数定义:

    • fibonacci(n int, c chan int) 是一个函数,它接受两个参数:一个整数 n 和一个整数通道 c
    • x, y := 0, 1 初始化Fibonacci数列的前两个数字,即0和1。
    • for i := 0; i < n; i++ 循环执行 n 次,每次迭代生成一个Fibonacci数字并将其发送到通道 c
    • c <- x 将当前的Fibonacci数字发送到通道。
    • x, y = y, x+y 更新 xy 的值以生成下一个Fibonacci数字。
    • close(c) 关闭通道,表示没有更多的值可以发送了。
  2. 主函数:

    • c := make(chan int, 10) 创建一个可以存储10个整数的通道。
    • go fibonacci(cap(c), c) 使用 cap(c) 作为参数来调用 fibonacci 函数,并使其在后台运行。
    • for i := range c 循环从通道 c 中接收值,并打印它们。由于通道被关闭后仍然可以接收值,但不会再次被关闭,所以这个循环会一直运行直到没有更多的值可以接收。

当你运行这段代码时,它会打印前10个Fibonacci数字:0, 1, 1, 2, 3, 5, 8, 13, 21, 34。

相关推荐
hkNaruto14 小时前
【P2P】【Go】采用go语言实现udp hole punching 打洞 传输速度测试 ping测试
golang·udp·p2p
入 梦皆星河14 小时前
go中常用的处理json的库
golang
海绵波波10717 小时前
Gin-vue-admin(2):项目初始化
vue.js·golang·gin
每天写点bug17 小时前
【go每日一题】:并发任务调度器
开发语言·后端·golang
一个不秃头的 程序员17 小时前
代码加入SFTP Go ---(小白篇5)
开发语言·后端·golang
基哥的奋斗历程17 小时前
初识Go语言
开发语言·后端·golang
ZVAyIVqt0UFji1 天前
go-zero负载均衡实现原理
运维·开发语言·后端·golang·负载均衡
唐墨1231 天前
golang自定义MarshalJSON、UnmarshalJSON 原理和技巧
开发语言·后端·golang
老大白菜1 天前
FastAPI vs Go 性能对比分析
开发语言·golang·fastapi
千年死缓1 天前
golang结构体转map
开发语言·后端·golang