Go语言-辟邪剑谱

2023-10-22 11:50
文章标签 语言 go 剑谱 辟邪

本文主要是介绍Go语言-辟邪剑谱,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

defer

defer 的使用

defer 方法

defer堆栈

defer 在实际中使用

 错误处理

错误示列

错误类型的内部实现

使用 New 函数创建自定义错误

Panic

recover

最佳实践

文件操作

读文件

写文件

反射

为什么需要反射

reflect 包

reflect.Type 和 reflect.Value

reflect.Kind

NumField() 和 Field() 方法

Int() 和 String() 方法

完整的程序


defer

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

defer 的使用

package mainimport (  "fmt"
)func finished() {  fmt.Println("Finished finding largest")
}func largest(nums []int) {  defer finished()    fmt.Println("Started finding largest")max := nums[0]for _, v := range nums {if v > max {max = v}}fmt.Println("Largest number in", nums, "is", max)
}func main() {  nums := []int{78, 109, 2, 563, 300}largest(nums)
}

在 12 行使用 defer 语句,意味着函数将在函数返回finished()之前被调用。

mian函数开始执行并打印上述输出的前两行。在它返回之前,我们的延迟函数finished执行并打印文本Finished finding largest:

Started finding largest  
Largest number in [78 109 2 563 300] is 563  
Finished finding largest 

defer 方法

Defer 不仅限于函数。延迟方法调用也是完全合法的。让我们写一个程序来测试一下。

package mainimport (  "fmt"
)type person struct {  firstName stringlastName string
}func (p person) fullName() {  fmt.Printf("%s %s",p.firstName,p.lastName)
}func main() {  p := person {firstName: "John",lastName: "Smith",}defer p.fullName()fmt.Printf("Welcome ")  
}

在上面的程序中,我们在第 22 行延迟了一个方法调用。 程序的其余部分很直观。该程序输出:Welcome John Smith

defer堆栈

当一个函数有多个 defer 调用时,它们会被压入堆栈并以后进先出 (LIFO) 的顺序执行。
我们将编写一个程序,它使用 defer 堆栈反向打印字符串。

package mainimport (  "fmt"
)func main() {  name := "Naveen"fmt.Printf("Original String: %s\n", string(name))fmt.Printf("Reversed String: ")for _, v := range []rune(name) {defer fmt.Printf("%c", v)}
}

在上面的程序中第11行,for range中的循环。迭代字符串并第12行调用defer fmt.Printf("%c", v)。 这些延迟调用将被添加到堆栈中。
请添加图片描述
上图表示添加 defer 调用后堆栈的内容。栈是后进先出的数据结构。最后压入堆栈的 defer 调用将被拉出并首先执行。在这种情况下defer fmt.Printf("%c", 'n'),将首先执行,因此字符串将以相反的顺序打印。

该程序将输出:

Original String: Naveen  
Reversed String: neevaN

defer 在实际中使用

程序员在编程的时候,经常需要打开一些资源,比如数据库连接、文件、锁等,这些资源需要在用完之后释放掉,否则会造成内存泄漏。

defer可以很方便地在函数结束前做一些清理操作。在打开资源语句的下一行,直接一句defer就可以在函数返回前关闭资源,相当优雅。

比如我们写一个读文件的函数:

func ReadFile(filename string) ([]byte, error) {f, err := os.Open(filename)if err != nil {return nil, err}defer f.Close()var n int64 = bytes.MinReadif fi, err := f.Stat(); err == nil {if size := fi.Size() + bytes.MinRead; size > n {n = size}}return readAll(f, n)
}

defer 修饰的 f.Close() 方法会在函数执行完成后或读取文件过程中抛出错误时执行,以确保已经打开的文件资源被关闭,从而避免内存泄露。

 错误处理

错误表示程序中出现异常情况。假设我们正在尝试打开一个文件,但该文件在文件系统中不存在。这是一种异常情况,它表示为错误。

Go 中的错误使用内置error类型表示。

错误示列

尝试打开一个不存在的文件:

package mainimport (  "fmt""os"
)func main() {  f, err := os.Open("/test.txt")if err != nil {fmt.Println(err)return}fmt.Println(f.Name(), "opened successfully")
}

在上面的程序的 9行 中,我们试图打开路径中的文件/test.txtos包的Open函数具有以下特性:

func Open(name string) (file *File, err error)

如果文件已成功打开,则 Open 函数将返回文件处理程序,并且错误将为 nil。如果打开文件时出错,将返回一个非零错误。

Go 中处理错误的惯用方法是将返回的错误与nil比较。 nil 值表示没有发生错误,非 nil 值表示存在错误。在我们的例子中,第 10 行检查了错误值是否为 nil。如果不是 nil,我们会简单地打印出错误,并在 main 函数中返回。

上面程序会输出:

open /test.txt: No such file or directory

错误类型的内部实现

看看内置error类型是如何定义的。error是具有以下定义的接口类型:

type error interface {  Error() string
}// http://golang.org/pkg/errors/error.go
// errors 构建 error 对象type errorString struct {s string
}func (e *errorString) Error() string {return e.s
}

error 有了一个签名为 Error() string 的方法。所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述。
fmt.Println打印错误时,函数在内部调用该Error() string 方法以获取错误的描述。这就是在第 11 行错误描述中的打印方式。

使用 New 函数创建自定义错误

创建自定义错误的最简单方法是使用errors包的New函数。

我们看下 New 函数是怎么实现的:

// Package errors implements functions to manipulate errors.package errors// New returns an error that formats as the given text.func New(text string) error {return &errorString{text}}// errorString is a trivial implementation of error.type errorString struct {s string}func (e *errorString) Error() string {return e.s}

实现非常简单。errorString是具有单个字符串s字段的结构类型。第 14 行使用了 errorString 指针接收器来实现 error 接口的 Error() string 方法。
第 5 行的 New() 函数有一个字符串参数,通过这个参数创建了 errorString 类型的变量,并返回了它的地址。于是它就创建并返回了一个新的错误。

我们看下如何使用:

package mainimport ("errors""fmt"
)func main() {err := errors.New("自定义错误")if err != nil {fmt.Print(err)}
}

最终会打印:自定义错误 。

Panic

有一些情况,程序在出现异常情况后无法继续执行。在这种情况下,我们使用panic提前终止程序。当一个函数遇到Panic时,它的执行被停止,任何延迟的函数都被执行,然后控制权返回给它的调用者。这个过程一直持续到当前goroutine的所有函数都返回,此时程序打印Panic消息,然后是堆栈跟踪,然后终止。

package mainimport (  "fmt"
)func fullName(firstName *string, lastName *string) {  if firstName == nil {panic("runtime error: first name cannot be nil")}if lastName == nil {panic("runtime error: last name cannot be nil")}fmt.Printf("%s %s\n", *firstName, *lastName)fmt.Println("returned normally from fullName")
}func main() {  firstName := "Elon"fullName(&firstName, nil)fmt.Println("returned normally from main")
}

上面是一个打印人全名的简单程序。第 7 行的 fullName 函数会打印出一个人的全名。该函数在第 8 行和第 11 行分别检查了 firstName 和 lastName 的指针是否为 nil。如果是 nilfullName 函数会调用含有不同的错误信息的 panic。当程序终止时,会打印出该错误信息。

recover

recover是一个内置函数,用于重新获得对Panic程序的控制权。

recover函数的声明如下:

func recover() interface{}

仅当在延迟函数中调用时,Recover 才有用。在延迟函数中执行恢复调用会通过恢复正常执行来停止Panic序列,并检索传递给Panic函数调用的错误消息。如果在延迟函数之外调用恢复,它不会停止Panic序列。

修改上面的示例,并在Panic后使用恢复来恢复正常执行。

package mainimport (  "fmt"
)func recoverFullName() {  if r := recover(); r!= nil {fmt.Println("recovered from ", r)}
}func fullName(firstName *string, lastName *string) {  defer recoverFullName()if firstName == nil {panic("runtime error: first name cannot be nil")}if lastName == nil {panic("runtime error: last name cannot be nil")}fmt.Printf("%s %s\n", *firstName, *lastName)fmt.Println("returned normally from fullName")
}func main() {  defer fmt.Println("deferred call in main")firstName := "Elon"fullName(&firstName, nil)fmt.Println("returned normally from main")
}

在第 7 行,recoverName() 函数调用了 recover(),返回了调用 panic 的参数。在这里,我们只是打印出 recover 的返回值(第 8 行)。在 fullName 函数内,我们在第 14 行延迟调用了 recoverNames()

当 fullName 发生 panic 时,会调用延迟函数 recoverName(),它使用了 recover() 来阻止 panic 后续事件。

最佳实践

  1. 在错误处理时,尽量不要使用 panic 和 recover。只有当程序不能继续运行的时候,才应该使用 panic 和 recover 机制。
  2. 当 if err != nil 时及时返回错误,从而避免过多的代码嵌套。

文件操作

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

读文件

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

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

Hello World. Welcome to file handling in Go.

读取示例如下:

package mainimport (  "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,并显示出文件的内容。

最后输出结果:

Contents of file: Hello World. Welcome to file handling in Go.

写文件

上面讲了如何读文件,现在讲下如何写入文件。

写文件的步骤如下:

  1. 创建文件
  2. 将字符串写入文件

示例代码:

package mainimport (  "fmt""os"
)func main() {  f, err := os.Create("test.txt") //create fileif err != nil {fmt.Println(err)return}l, err := f.WriteString("Hello World") // writing fileif err != nil {fmt.Println(err)f.Close()return}fmt.Println(l, "bytes written successfully")err = f.Close()if err != nil {fmt.Println(err)return}
}

上面程序的第9行中的create函数创建了一个名为test.txt的文件。如果存在同名文件,则create函数将截断该文件。此函数返回文件错误描述。

在第 14 行,我们使用WriteString方法将字符串Hello World写入文件。此方法返回写入的字节数和错误(如果有)。

最后,我们在第21行关闭文件。执行完后,您可以在执行该程序的目录中找到一个名为test.txt的文件。如果您使用任何文本编辑器打开该文件,你会发现它包含文本Hello World


反射

反射是 Go 中的高级用法之一。是程序在运行时检查其变量和值并找到它们的类型的能力。

为什么需要反射

任何人在学习反射时都会遇到的第一个问题是,当我们程序中的每个变量都由我们定义并且我们在编译时本身就知道它的类型时,为什么我们甚至需要在运行时检查一个变量并找到它的类型。嗯,这在大多数情况下都是正确的,但并非总是如此。

假设我们要编写一个简单的函数,它将一个结构作为参数,并使用它创建一个 SQL 插入查询:

package mainimport (  "fmt"
)type order struct {  ordId      intcustomerId int
}func main() {  o := order{ordId:      1234,customerId: 567,}fmt.Println(o)
}

我们需要编写一个函数,将上面程序中的结构o作为参数,并返回以下 SQL 插入查询:

insert into order values(1234, 567)  

这个函数写起来很简单。现在让我们这样做。

package mainimport (  "fmt"
)type order struct {  ordId      intcustomerId int
}func createQuery(o order) string {  i := fmt.Sprintf("insert into order values(%d, %d)", o.ordId, o.customerId)return i
}func main() {  o := order{ordId:      1234,customerId: 567,}fmt.Println(createQuery(o))
}

函数createQuery通过使用结构体o中的ordIdcustomerId字段创建了SQL插入语句。该程序将输出:

insert into order values(1234, 567)  

现在我们来升级这个sql语句生成器。如果我们想让它变得通用,可以适用于任何结构体类型该怎么办呢?

package maintype order struct {  ordId      intcustomerId int
}type employee struct {  name stringid intaddress stringsalary intcountry string
}func createQuery(q interface{}) string {  
}func main() {}

我们的目标就是完成 createQuery 函数(上述程序中的第 16 行),它可以接收任何结构体作为参数,根据结构体的字段创建插入查询。
例如,如果我们传递下面的结构,

o := order {  ordId: 1234,customerId: 567
}

我们的createQuery函数应该返回:

insert into order values (1234, 567)  

同样,如果我们传入:

 e := employee {name: "Naveen",id: 565,address: "Science Park Road, Singapore",salary: 90000,country: "Singapore",}

它应该返回:

insert into employee values("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore")  

由于该createQuery函数应该适用于任何结构,因此它需要一个interface{}作为参数。为简单起见,我们只处理包含 string 和 int 类型字段的结构体,但可以扩展为包含任何类型的字段。

createQuery 函数应该适用于所有的结构体。编写此函数的唯一方法是在运行时检查传递给它的struct参数的类型,找到它的字段然后创建sql语句。这时就需要用到反射了。

reflect 包

reflect包在 Go 中实现了运行时反射。reflect 包有助于识别底层的具体类型和interface{}变量的值。这正是我们所需要的。该createQuery函数接受一个interface{}参数,并且需要根据参数的具体类型和值创建查询interface{}。这正是反射包的作用。

在编写通用查询生成器程序之前,我们需要首先了解反射包中的一些类型和方法。让我们一一看看。

reflect.Type 和 reflect.Value

参数 interface{} 的具体类型由 reflect.Type 表示,而 reflect.Value 表示它的具体值。reflect.TypeOf() 和 reflect.ValueOf() 两个函数可以分别返回 reflect.Type 和 reflect.Value。这两种类型是我们创建sql生成器的基础。我们现在用一个简单的例子来理解这两种类型。

package mainimport (  "fmt""reflect"
)type order struct {  ordId      intcustomerId int
}func createQuery(q interface{}) {  t := reflect.TypeOf(q)v := reflect.ValueOf(q)fmt.Println("Type ", t)fmt.Println("Value ", v)}
func main() {  o := order{ordId:      456,customerId: 56,}createQuery(o)}

在上面的程序中,第 13 行的 createQuery 函数接收 interface{} 作为参数。在第 14行,reflect.TypeOf 接收了参数 interface{},返回了reflect.Type,它包含了传入的 interface{} 参数的具体类型。同样地,在第 15 行,reflect.ValueOf函数接收参数 interface{},并返回了 reflect.Value,它包含了传来的 interface{} 的具体值。

上面的程序打印:

Type  main.order  
Value  {456 56}  

从输出中,我们可以看到程序打印出接口的具体类型和值。

reflect.Kind

反射包中还有一种更重要的类型,称为Kind。

反射包中的类型Kind和Type可能看起来很相似,但它们之间存在差异,这可以从下面的程序中清楚地看出:

package mainimport (  "fmt""reflect"
)type order struct {  ordId      intcustomerId int
}func createQuery(q interface{}) {  t := reflect.TypeOf(q)k := t.Kind()fmt.Println("Type ", t)fmt.Println("Kind ", k)}
func main() {  o := order{ordId:      456,customerId: 56,}createQuery(o)}

上面的程序输出:

Type  main.order  
Kind  struct 

我想您现在将清楚两者之间的差异。Type表示 interface{} 的实际类型,在本例中为 main.Order,Kind表示该类型的具体种类。在这种情况下,它是一个struct

NumField() 和 Field() 方法

NumField()方法返回结构中的字段数,Field(i int)方法返回字段 i 的 reflect.Value

package mainimport (  "fmt""reflect"
)type order struct {  ordId      intcustomerId int
}func createQuery(q interface{}) {  if reflect.ValueOf(q).Kind() == reflect.Struct {v := reflect.ValueOf(q)fmt.Println("Number of fields", v.NumField())for i := 0; i < v.NumField(); i++ {fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))}}}
func main() {  o := order{ordId:      456,customerId: 56,}createQuery(o)
}

在上面的程序中,因为 NumField 方法只能在结构体上使用,我们在第 14行首先检查了 q 的类别是 struct。其他代码很容易看懂,不再作解释。该程序会输出:

Number of fields 2  
Field:0 type:reflect.Value value:456  
Field:1 type:reflect.Value value:56  

Int() 和 String() 方法

Int 和 String 可以帮助我们分别取出 reflect.Value 为 int64 和 string的值。

package mainimport (  "fmt""reflect"
)func main() {  a := 56x := reflect.ValueOf(a).Int()fmt.Printf("type:%T value:%v\n", x, x)b := "Naveen"y := reflect.ValueOf(b).String()fmt.Printf("type:%T value:%v\n", y, y)}

上面程序第 10 行,我们取出 reflect.Value,并转换为 int64,而在第 13 行,我们取出 reflect.Value 并将其转换为 string。该程序会输出:

type:int64 value:56  
type:string value:Naveen 

完整的程序

现在我们sql生成器了,补充完整:

package mainimport (  "fmt""reflect"
)type order struct {  ordId      intcustomerId int
}type employee struct {  name    stringid      intaddress stringsalary  intcountry string
}func createQuery(q interface{}) {  if reflect.ValueOf(q).Kind() == reflect.Struct {t := reflect.TypeOf(q).Name()query := fmt.Sprintf("insert into %s values(", t)v := reflect.ValueOf(q)for i := 0; i < v.NumField(); i++ {switch v.Field(i).Kind() {case reflect.Int:if i == 0 {query = fmt.Sprintf("%s%d", query, v.Field(i).Int())} else {query = fmt.Sprintf("%s, %d", query, v.Field(i).Int())}case reflect.String:if i == 0 {query = fmt.Sprintf("%s\"%s\"", query, v.Field(i).String())} else {query = fmt.Sprintf("%s, \"%s\"", query, v.Field(i).String())}default:fmt.Println("Unsupported type")return}}query = fmt.Sprintf("%s)", query)fmt.Println(query)return}fmt.Println("unsupported type")
}func main() {  o := order{ordId:      456,customerId: 56,}createQuery(o)e := employee{name:    "Naveen",id:      565,address: "Coimbatore",salary:  90000,country: "India",}createQuery(e)i := 90createQuery(i)}

上面第 22 行,我们首先检查了传来的参数是否是一个结构体。在第 23 行,我们使用了 Name() 方法,从该结构体的 reflect.Type 获取了结构体的名字。接下来一行,我们用 t 来创建sql

在第 28 行,case 语句 检查了当前字段是否为 reflect.Int,如果是的话,我们会取到该字段的值,并使用 Int()方法转换为 int64if else 语句 用于处理边界情况。请添加日志来理解为什么需要它。在第 34 行,我们用来相同的逻辑来取到 string

我们还作了额外的检查,以防止 createQuery 函数传入不支持的类型时,程序发生崩溃。程序的其他代码是很容易理解的,这里不再解释。我建议你在合适的地方添加日志,检查输出,来更好地理解这个程序。

该程序打印:

insert into order values(456, 56)  
insert into employee values("Naveen", 565, "Coimbatore", 90000, "India")  
unsupported type  

至于向sql中添加字段名,你可以尝试着修改程序,打印出以下格式的sql

insert into order(ordId, customerId) values(456, 56)  

注意:

  • 反射是 Go 中一个非常强大和先进的概念,应该谨慎使用。使用反射编写清晰且可维护的代码非常困难。应尽可能避免使用,仅在绝对必要时使用。

这篇关于Go语言-辟邪剑谱的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/261257

相关文章

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

go中空接口的具体使用

《go中空接口的具体使用》空接口是一种特殊的接口类型,它不包含任何方法,本文主要介绍了go中空接口的具体使用,具有一定的参考价值,感兴趣的可以了解一下... 目录接口-空接口1. 什么是空接口?2. 如何使用空接口?第一,第二,第三,3. 空接口几个要注意的坑坑1:坑2:坑3:接口-空接口1. 什么是空接

C语言中的数据类型强制转换

《C语言中的数据类型强制转换》:本文主要介绍C语言中的数据类型强制转换方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C语言数据类型强制转换自动转换强制转换类型总结C语言数据类型强制转换强制类型转换:是通过类型转换运算来实现的,主要的数据类型转换分为自动转换

利用Go语言开发文件操作工具轻松处理所有文件

《利用Go语言开发文件操作工具轻松处理所有文件》在后端开发中,文件操作是一个非常常见但又容易出错的场景,本文小编要向大家介绍一个强大的Go语言文件操作工具库,它能帮你轻松处理各种文件操作场景... 目录为什么需要这个工具?核心功能详解1. 文件/目录存javascript在性检查2. 批量创建目录3. 文件

C语言实现两个变量值交换的三种方式

《C语言实现两个变量值交换的三种方式》两个变量值的交换是编程中最常见的问题之一,以下将介绍三种变量的交换方式,其中第一种方式是最常用也是最实用的,后两种方式一般只在特殊限制下使用,需要的朋友可以参考下... 目录1.使用临时变量(推荐)2.相加和相减的方式(值较大时可能丢失数据)3.按位异或运算1.使用临时

使用C语言实现交换整数的奇数位和偶数位

《使用C语言实现交换整数的奇数位和偶数位》在C语言中,要交换一个整数的二进制位中的奇数位和偶数位,重点需要理解位操作,当我们谈论二进制位的奇数位和偶数位时,我们是指从右到左数的位置,本文给大家介绍了使... 目录一、问题描述二、解决思路三、函数实现四、宏实现五、总结一、问题描述使用C语言代码实现:将一个整

C语言字符函数和字符串函数示例详解

《C语言字符函数和字符串函数示例详解》本文详细介绍了C语言中字符分类函数、字符转换函数及字符串操作函数的使用方法,并通过示例代码展示了如何实现这些功能,通过这些内容,读者可以深入理解并掌握C语言中的字... 目录一、字符分类函数二、字符转换函数三、strlen的使用和模拟实现3.1strlen函数3.2st

Go语言中最便捷的http请求包resty的使用详解

《Go语言中最便捷的http请求包resty的使用详解》go语言虽然自身就有net/http包,但是说实话用起来没那么好用,resty包是go语言中一个非常受欢迎的http请求处理包,下面我们一起来学... 目录安装一、一个简单的get二、带查询参数三、设置请求头、body四、设置表单数据五、处理响应六、超

C语言中的浮点数存储详解

《C语言中的浮点数存储详解》:本文主要介绍C语言中的浮点数存储详解,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、首先明确一个概念2、接下来,讲解C语言中浮点型数存储的规则2.1、可以将上述公式分为两部分来看2.2、问:十进制小数0.5该如何存储?2.3 浮点

Golang基于内存的键值存储缓存库go-cache

《Golang基于内存的键值存储缓存库go-cache》go-cache是一个内存中的key:valuestore/cache库,适用于单机应用程序,本文主要介绍了Golang基于内存的键值存储缓存库... 目录文档安装方法示例1示例2使用注意点优点缺点go-cache 和 Redis 缓存对比1)功能特性