掘金 后端 ( ) • 2024-04-18 13:46

theme: condensed-night-purple

1.1 反射reflect

1.1.1 调试工具 dlv

GitHub:

https://github.com/go-delve/delve

下载方式:

go install github.com/go-delve/delve/cmd/dlv@latest

之后就是配置环境变量了,自行搜索下即可。

Windows:

https://learn.microsoft.com/zh-cn/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-7.3

安装完成后,我们在终端命令行内任意一个路径下输入dlv,则会出现这样的提示:

image-20231021200013792.png

这样的话,就已经配置完成了。

接下来,我们来看看dlv内都有哪些内容:

  • 第一步

    dlv debug main.go
    

    这个意思是,在main.go文件中,开启debug模式

  • 第二步,输入help,就会出现下方代码块中的内容

(dlv) help
The following commands are available:

Running the program:
   call ------------------------ Resumes process, injecting a function call (EXPERIMENTAL!!!)
   continue (alias: c) --------- Run until breakpoint or program termination.
   next (alias: n) ------------- Step over to next source line.
   rebuild --------------------- Rebuild the target executable and restarts it. It does not work if the executable was not built by delve.
   restart (alias: r) ---------- Restart process.
   step (alias: s) ------------- Single step through program.
   step-instruction (alias: si)  Single step a single cpu instruction.
   stepout (alias: so) --------- Step out of the current function.

Manipulating breakpoints:
   break (alias: b) ------- Sets a breakpoint.
   breakpoints (alias: bp)  Print out info for active breakpoints.
   clear ------------------ Deletes breakpoint.
   clearall --------------- Deletes multiple breakpoints.
   condition (alias: cond)  Set breakpoint condition.
   on --------------------- Executes a command when a breakpoint is hit.
   toggle ----------------- Toggles on or off a breakpoint.
   trace (alias: t) ------- Set tracepoint.
   watch ------------------ Set watchpoint.

Viewing program variables and memory:
   args ----------------- Print function arguments.
   display -------------- Print value of an expression every time the program stops.
   examinemem (alias: x)  Examine raw memory at the given address.
   locals --------------- Print local variables.
   print (alias: p) ----- Evaluate an expression.
   regs ----------------- Print contents of CPU registers.
   set ------------------ Changes the value of a variable.
   vars ----------------- Print package variables.
   whatis --------------- Prints type of an expression.

Listing and switching between threads and goroutines:
   goroutine (alias: gr) -- Shows or changes current goroutine
   goroutines (alias: grs)  List program goroutines.
   thread (alias: tr) ----- Switch to the specified thread.
   threads ---------------- Print out info for every traced thread.

Viewing the call stack and selecting frames:
   deferred --------- Executes command in the context of a deferred call.
   down ------------- Move the current frame down.
   frame ------------ Set the current frame, or execute command on a different frame.
   stack (alias: bt)  Print stack trace.
   up --------------- Move the current frame up.

Other commands:
   config --------------------- Changes configuration parameters.
   disassemble (alias: disass)  Disassembler.
   dump ----------------------- Creates a core dump from the current process state
   edit (alias: ed) ----------- Open where you are in $DELVE_EDITOR or $EDITOR
   exit (alias: quit | q) ----- Exit the debugger.
   funcs ---------------------- Print list of functions.
   help (alias: h) ------------ Prints the help message.
   libraries ------------------ List loaded dynamic libraries
   list (alias: ls | l) ------- Show source code.
   packages ------------------- Print list of packages.
   source --------------------- Executes a file containing a list of delve commands
   sources -------------------- Print list of source files.
   target --------------------- Manages child process debugging.
   transcript ----------------- Appends command output to a file.
   types ---------------------- Print list of types

Type help followed by a command for full documentation.
(dlv)

常用的几个命令:

break,breakpoints ,clear,clearall,continue,next,print分别是: 设置断点、打印设置的断点、清除断点、清除断点、运行程序遇到断点或终止、执行下一步操作、输出

1.1.2 接口的底层实现

在Go语言中,接口类型使用两种内部结构来表示,一个是iface,用于非空接口(即包含方法的接口),另一个是eface,用于空接口(即不包含任何方法的接口)。

  1. iface 结构

    • iface结构用于表示非空接口(包含方法的接口)。它包含两个字段:

      • type:指向实现该接口的具体类型的指针。
      • value:指向实现了接口方法的具体值的指针。

    这个结构用于在运行时实现接口的动态分派,确保调用的方法是正确的。

MjAyMy04LTIwLTEyLTU3LTIxLTE1OA==.png

package main

type Rectangle struct {
	Width  int
	Height int
}

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

type Shape interface {
	Area()
}

func main() {
	r := Rectangle{
		Width:  10,
		Height: 8,
	}
	var s Shape
	var i interface{} = s
	_ = r
	_ = i
}
Type 'help' for list of commands.
(dlv)
(dlv) clear
Command failed: not enough arguments
(dlv) clearall
(dlv) b main.main
Breakpoint 1 set at 0x104a71910 for main.main() ./main.go:16
(dlv) c
> main.main() ./main.go:16 (hits goroutine(1):1 total:1) (PC: 0x104a71910)
    11:	
    12:	type Shape interface {
    13:		Area()
    14:	}
    15:	
=>  16:	func main() {
    17:		r := Rectangle{
    18:			Width:  10,
    19:			Height: 8,
    20:		}
    21:		var s Shape
(dlv) n
> main.main() ./main.go:17 (PC: 0x104a7191c)
    12:	type Shape interface {
    13:		Area()
    14:	}
    15:	
    16:	func main() {
=>  17:		r := Rectangle{
    18:			Width:  10,
    19:			Height: 8,
    20:		}
    21:		var s Shape
    22:		var i interface{} = s
(dlv) n
> main.main() ./main.go:18 (PC: 0x104a71920)
    13:		Area()
    14:	}
    15:	
    16:	func main() {
    17:		r := Rectangle{
=>  18:			Width:  10,
    19:			Height: 8,
    20:		}
    21:		var s Shape
    22:		var i interface{} = s
    23:		_ = r
(dlv) n
> main.main() ./main.go:19 (PC: 0x104a71928)
    14:	}
    15:	
    16:	func main() {
    17:		r := Rectangle{
    18:			Width:  10,
=>  19:			Height: 8,
    20:		}
    21:		var s Shape
    22:		var i interface{} = s
    23:		_ = r
    24:		_ = i
(dlv) n
> main.main() ./main.go:21 (PC: 0x104a71930)
    16:	func main() {
    17:		r := Rectangle{
    18:			Width:  10,
    19:			Height: 8,
    20:		}
=>  21:		var s Shape
    22:		var i interface{} = s
    23:		_ = r
    24:		_ = i
    25:	}
(dlv) n
> main.main() ./main.go:22 (PC: 0x104a71934)
    17:		r := Rectangle{
    18:			Width:  10,
    19:			Height: 8,
    20:		}
    21:		var s Shape
=>  22:		var i interface{} = s
    23:		_ = r
    24:		_ = i
    25:	}
(dlv) n
> main.main() ./main.go:25 (PC: 0x104a7196c)
    20:		}
    21:		var s Shape
    22:		var i interface{} = s
    23:		_ = r
    24:		_ = i
=>  25:	}
(dlv) p i
interface {} nil
(dlv) p &i
(*interface {})(0x14000044728)
(dlv) p r
main.Rectangle {Width: 10, Height: 8}
(dlv) p &r
(*main.Rectangle)(0x14000044700)
(dlv) p *(*runtime.iface)((uintptr)(&r))
runtime.iface {
	tab: *runtime.itab {
		inter: (unreadable protocol error E08 during memory read for packet $ma,20),
		_type: (unreadable protocol error E08 during memory read for packet $ma,20),
		hash: (unreadable protocol error E08 during memory read for packet $ma,20),
		_: [4]uint8 [(unreadable protocol error E08 during memory read for packet $ma,20),(unreadable protocol error E08 during memory read for packet $ma,20),(unreadable protocol error E08 during memory read for packet $ma,20),(unreadable protocol error E08 during memory read for packet $ma,20)],
		fun: [1]uintptr [(unreadable protocol error E08 during memory read for packet $ma,20)],},
	data: unsafe.Pointer(0x8),}
(dlv) p *(*runtime.eface)((uintptr)(&i))
runtime.eface {
	_type: *internal/abi.Type nil,
	data: unsafe.Pointer(0x0),}
(dlv)
  1. eface 结构

    • eface结构用于表示空接口(不包含任何方法的接口)。它包含两个字段:

      • type:指向实际值的类型的指针。
      • data:指向实际值的指针。

    这个结构允许空接口变量存储任意类型的值,因为它不包含任何方法,可以代表任何类型。

MjAyMy04LTIwLTEyLTU3LTI0LTQ1MQ==.png

package main

func main() {
	num := 123
	var i interface{} = &num
	_ = i
}
  1. 我们先写出这样的代码,然后通过dlv命令来调试一下代码,这样我们可以直接了当的看出其中的底层实现。

    其次,通过之前学习的内容,进行调试:

    Type 'help' for list of commands.
    (dlv) b main.main
    Breakpoint 1 set at 0x10457d910 for main.main() ./main.go:3
    (dlv) bp
    Breakpoint runtime-fatal-throw (enabled) at 0x104552ef0,0x104552fb0,0x1045672ec for (multiple functions)() <multiple locations>:0 (0)
    Breakpoint unrecovered-panic (enabled) at 0x104553270 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1175 (0)
      print runtime.curg._panic.arg
    Breakpoint 1 (enabled) at 0x10457d910 for main.main() ./main.go:3 (0)
    (dlv) c
    > main.main() ./main.go:3 (hits goroutine(1):1 total:1) (PC: 0x10457d910)
         1: package main
         2: 
    =>   3: func main() {
         4:   num := 123
         5:   var i interface{} = &num
         6:   _ = i
         7: }
         8: 
    (dlv) n
    > main.main() ./main.go:4 (PC: 0x10457d91c)
         1: package main
         2: 
         3: func main() {
    =>   4:   num := 123
         5:   var i interface{} = &num
         6:   _ = i
         7: }
         8: 
         9: //type Rectangle struct {
    (dlv) n
    > main.main() ./main.go:5 (PC: 0x10457d924)
         1: package main
         2: 
         3: func main() {
         4:   num := 123
    =>   5:   var i interface{} = &num
         6:   _ = i
         7: }
         8: 
         9: //type Rectangle struct {
        10: //  Width  int
    (dlv) n
    > main.main() ./main.go:7 (PC: 0x10457d93c)
         2: 
         3: func main() {
         4:   num := 123
         5:   var i interface{} = &num
         6:   _ = i
    =>   7: }
         8: 
         9: //type Rectangle struct {
        10: //  Width  int
        11: //  Height int
        12: //}
    (dlv) p num
    123
    (dlv) p &num
    (*int)(0x14000044728)
    (dlv) p i
    interface {}(*int) *123
    (dlv) p (&i)
    (*interface {})(0x14000044738)
    (dlv) p (unitptr)(&i)
    Command failed: could not evaluate function or type (unitptr): could not find symbol value for unitptr
    (dlv) p (uintptr)(&i)
    1374389815096
    (dlv) p (*runtime.eface)((uintptr)(&i))
    (*runtime.eface)(0x14000044738)
    (dlv) p *(*runtime.eface)((uintptr)(&i))
    runtime.eface {
      _type: *internal/abi.Type {Size_: 8, PtrBytes: 8, Hash: 3126353255, TFlag: TFlagRegularMemory (8), Align_: 8, FieldAlign_: 8, Kind_: 54, Equal: runtime.memequal64, GCData: *1, Str: 362, PtrToThis: 0},
      data: unsafe.Pointer(0x14000044728),}
    (dlv)
    

    最后就会调测出:_type以及data

这两种结构是Go语言运行时系统用来实现接口和类型断言的内部机制。它们使得Go语言的接口实现非常灵活,并且可以适应不同类型的对象。用户在使用接口时,一般不需要直接与这些结构交互,而是使用Go语言提供的接口和类型断言语法来处理。因为这不是Go语言中合法的操作。所以,用到了反射reflect

1.1.3 反射

reflect 包是Go语言标准库中的一个强大工具,它允许你在运行时检查和操作变量、方法、结构等信息,而无需在编译时知道这些信息的确切类型。使用 reflect 包,你可以编写更加灵活和通用的代码,但同时也需要小心,因为它牵涉到运行时的类型信息。

在Go语言的reflect包中,reflect.TypeOfreflect.ValueOf 是两个非常重要的函数,它们分别用于获取一个变量的类型信息和获取一个变量的反射对象(reflect.Value)。

1.1.3.1 reflect.TypeOf 函数

reflect.TypeOf 函数用于获取一个变量的类型信息,返回一个 reflect.Type 类型的对象。这个函数的签名如下:

func TypeOf(i interface{}) Type

其中,i 是要获取类型信息的变量。下面是一个示例:

package main
​
import (
  "fmt"
  "reflect"
)
​
func main() {
  var x float64 = 3.14
  fmt.Println(reflect.TypeOf(x)) // 输出: float64
}

在这个例子中,reflect.TypeOf(x) 返回的是 float64 类型的反射对象。

1.1.3.1.1 TypeOf 函数常用方法--Name

Name:这将返回类型的名称。切片或指针,没有名称,此方法返回一个空字符串。

package main
​
import (
  "fmt"
  "reflect"
)
​
type Rectangle struct {
  Width  int
  Height int
}
​
func main() {
  r := Rectangle{
    Width:  10,
    Height: 8,
  }
  fmt.Println(reflect.TypeOf(r).Name())
}

image-20231022102322939.png

1.1.3.1.2 TypeOf 函数常用方法--Kind
package main
​
import (
  "fmt"
  "reflect"
)
​
type Rectangle struct {
  Width  int
  Height int
}
​
func main() {
  r := Rectangle{
    Width:  10,
    Height: 8,
  }
  fmt.Println(reflect.TypeOf(r).Kind() == reflect.Struct)
  fmt.Println(reflect.TypeOf(&r).Kind() == reflect.Pointer)
  fmt.Println(reflect.TypeOf(true).Kind() == reflect.Bool)
  fmt.Println(reflect.TypeOf([]int{}).Kind() == reflect.Slice)
  fmt.Println(reflect.TypeOf(map[string]int{}).Kind() == reflect.Map)
}

image-20231022102658834.png

1.1.3.1.3 TypeOf 函数常用方法--Elem

Elem: 如果变量是指针、map、slice、channel或数组,使用varType.Elem() 找出元素的类型。

package main
​
import (
  "fmt"
  "reflect"
)
​
type Rectangle struct {
  Width  int
  Height int
}
​
func main() {
  r := Rectangle{
    Width:  10,
    Height: 8,
  }
  fmt.Println(reflect.TypeOf(r).Name())
  fmt.Println(reflect.TypeOf(&r).Elem())
  fmt.Println(reflect.TypeOf([]Rectangle{}).Elem())
  fmt.Println(reflect.TypeOf([]*Rectangle{}).Elem())
  fmt.Println(reflect.TypeOf(map[string]*Rectangle{}).Elem().Elem())
  fmt.Println(reflect.TypeOf(make(chan Rectangle)).Elem())
}

image-20231022102842708.png

举两个🌰

package main
​
import (
  "fmt"
  "reflect"
)
​
type Person struct {
  Name string
  Age  int
}
​
func main() {
  p := Person{Name: "Alice", Age: 30}
  t := reflect.TypeOf(p)
​
  // 使用 Name 方法获取类型的名称
  fmt.Println("Type Name:", t.Name()) // 输出: Person
​
  // 使用 Kind 方法获取类型的种类
  fmt.Println("Type Kind:", t.Kind()) // 输出: struct
​
  // 获取结构体字段的类型信息
  nameField, _ := t.FieldByName("Name")
  ageField, _ := t.FieldByName("Age")
​
  // 使用 Elem 方法获取字段的元素类型
  fmt.Println("Name Field Type:", nameField.Type) // 输出: string
  fmt.Println("Age Field Type:", ageField.Type)   // 输出: int
}

这个例子中:我们定义了一个 Person 结构体,包含了两个字段 NameAge。我们使用 reflect.TypeOf 函数获取了结构体 Person 的反射类型信息,并且通过 Name 方法获得了类型的名称("Person"),通过 Kind 方法获得了类型的种类("struct")。然后,我们使用 FieldByName 方法获取了结构体字段 NameAge 的类型信息,最后使用 Elem 方法获得了字段的元素类型。

package main
​
import (
  "fmt"
  "reflect"
)
​
type Person struct {
  Name string
  Age  int
}
​
func main() {
  p := Person{Name: "Alice", Age: 30}
  t := reflect.TypeOf(p)
​
  // 判断类型的种类(Kind)
  if t.Kind() == reflect.Struct {
    // 获取结构体的元素类型
    elemType := t.Elem()
​
    // 打印元素类型的名称和种类
    fmt.Println("元素类型的名称:", elemType.Name()) // 输出: ""
    fmt.Println("元素类型的种类:", elemType.Kind()) // 输出: struct
  }
}

在这个示例中,tPerson 结构体的反射类型。通过 Elem() 方法,我们获取了结构体的元素类型。需要注意的是,结构体的元素类型并不是结构体本身,而是结构体的成员字段的类型。

在这里,elemType.Name() 返回的是空字符串,因为结构体本身并没有名称。而 elemType.Kind() 返回的是 struct,表示元素类型是一个结构体。

请注意,Elem() 方法只能用于接口、指针、数组、切片、通道类型。如果调用了不支持的类型,会引发 panic。在实际使用时,请确保你的代码遵循正确的类型规则。

1.1.3.1.4 获取结构体类型中的字段数量和具体字段的信息

在Go的reflect包中,Type 类型具有 NumField() 方法和 Field(int) 方法,可以用于获取结构体类型中的字段数量和具体字段的信息。

1.NumField() 方法

NumField() 方法用于获取结构体中的字段数量。它返回一个 int 值,表示结构体中的字段数量。以下是一个示例:

package main
​
import (
  "fmt"
  "reflect"
)
​
type Person struct {
  Name string
  Age  int
}
​
func main() {
  p := Person{Name: "Alice", Age: 30}
  t := reflect.TypeOf(p)
​
  // 获取结构体中的字段数量
  numFields := t.NumField()
  fmt.Println("字段数量:", numFields) // 输出: 2
}

在这个示例中,t.NumField() 返回的是 Person 结构体中的字段数量。

2.Field(int) 方法

Field(int) 方法用于获取结构体中指定索引位置的字段信息。索引从0开始,一直到 NumField()-1。该方法返回一个 reflect.StructField 类型的对象,其中包含了字段的详细信息,比如字段的名称、类型等。以下是一个示例:

package main
​
import (
  "fmt"
  "reflect"
)
​
type Person struct {
  Name string
  Age  int
}
​
func main() {
  p := Person{Name: "Alice", Age: 30}
  t := reflect.TypeOf(p)
​
  // 获取第一个字段的信息
  firstField := t.Field(0)
  fmt.Println("字段名称:", firstField.Name) // 输出: Name
  fmt.Println("字段类型:", firstField.Type) // 输出: string
}

在这个示例中,t.Field(0) 获取的是 Person 结构体中的第一个字段(即 Name 字段)的信息。

需要注意的是,Field(int) 方法的参数是字段的索引,从0开始。这个方法返回的是一个 reflect.StructField 类型的对象,你可以通过它获取字段的各种信息。

3.举个🌰

package main
​
import (
  "fmt"
  "reflect"
)
​
type Rectangle struct {
  Width     int
  Height    int
  Address   string
  Assistant string
}
​
func main() {
  r := Rectangle{
    Width:  10,
    Height: 8,
  }
  rt := reflect.TypeOf(r)
  for i := 0; i < rt.NumField(); i++ {
    field := rt.Field(i)
    fmt.Println(field.Name)
  }
}
1.1.3.2 reflect.ValueOf 函数

reflect.ValueOf 函数用于获取一个变量的反射对象(reflect.Value),返回一个 reflect.Value 类型的对象。这个函数的签名如下:

func ValueOf(i interface{}) Value

其中,i 是要获取反射对象的变量。下面是一个示例:

package main
​
import (
  "fmt"
  "reflect"
)
​
func main() {
  var x float64 = 3.14
  value := reflect.ValueOf(x)
  fmt.Println(value.Float()) // 输出: 3.14
}

在这个例子中,reflect.ValueOf(x) 返回的是 float64 类型的反射对象,你可以使用 value.Float() 来获取这个变量的实际值。

1.1.3.2.1 ValueOf 函数示例
package main
​
import (
  "fmt"
  "reflect"
)
​
func main() {
  var x float64 = 3.14
​
  // 使用reflect.ValueOf获取变量x的reflect.Value
  valueOfX := reflect.ValueOf(x)
​
  // 获取reflect.Value的类型
  fmt.Println("Type of valueOfX:", valueOfX.Type())
​
  // 获取reflect.Value的kind
  fmt.Println("Kind of valueOfX:", valueOfX.Kind())
​
  // 获取reflect.Value的值
  fmt.Println("Value of valueOfX:", valueOfX.Float())
​
  // 尝试修改reflect.Value的值(会引发panic,因为reflect.Value是不可修改的)
  // valueOfX.SetFloat(2.71) // 这行代码会引发panic
​
  // 使用Elem方法获取指针指向的值的reflect.Value
  ptr := &x
  valueOfPtr := reflect.ValueOf(ptr).Elem()
  fmt.Println("Value of valueOfPtr (before modification):", valueOfPtr.Float())
​
  // 使用reflect.Value修改指针指向的值
  valueOfPtr.SetFloat(2.71)
  fmt.Println("Value of x after modification:", x)
}

在这个示例中,首先使用reflect.ValueOf函数获取了一个float64类型变量xreflect.Value。然后,使用Value类型的Type方法获取了该reflect.Value的类型,使用Kind方法获取了该reflect.Value的Kind(基础类型),使用Float方法获取了该reflect.Value的实际值。接着,演示了如何使用Elem方法获取指针指向的值的reflect.Value,并且修改了该指针指向的值。需要注意的是,reflect.Value是不可修改的,如果尝试修改将会引发panic。所以,如果需要修改变量的值,必须使用指针。


1.1.3.2.2ValueOf以及TypeOf 函数示例

当你需要处理不同类型的结构体时,可以使用 reflect.TypeOf 获取结构体的类型信息,以及使用 reflect.ValueOf 获取结构体的值。以下是一个示例,演示了如何创建一个通用的结构体操作函数,该函数接受一个结构体类型的参数,使用 reflect 包中的函数进行操作:

package main
​
import (
  "fmt"
  "reflect"
)
​
type Person struct {
  Name  string
  Age   int
  Email string
}
​
func processStruct(input interface{}) {
  // 使用 reflect.TypeOf 获取结构体的类型信息
  structType := reflect.TypeOf(input)
  fmt.Println("Struct Type:", structType)
​
  // 使用 reflect.ValueOf 获取结构体的值
  structValue := reflect.ValueOf(input)
​
  // 遍历结构体的字段并打印字段名和对应的值
  for i := 0; i < structType.NumField(); i++ {
    field := structType.Field(i)
    fieldValue := structValue.Field(i)
​
    // 打印字段名和对应的值
    fmt.Printf("Field: %s, Value: %v\n", field.Name, fieldValue.Interface())
  }
}
​
func main() {
  // 创建一个Person结构体实例
  person := Person{
    Name:  "Alice",
    Age:   30,
    Email: "[email protected]",
  }
​
  // 调用处理结构体的函数
  processStruct(person)
}

在这个示例中,我们定义了一个 Person 结构体,然后编写了一个名为 processStruct 的函数,该函数接受一个空接口类型的参数 input。在函数内部,我们使用 reflect.TypeOf 获取结构体的类型信息,然后使用 reflect.ValueOf 获取结构体的值。接着,我们使用 NumField 方法获取结构体的字段数量,然后遍历字段,分别获取字段名和对应的值,并打印出来。


总之,reflect.TypeOf 用于获取类型信息,而 reflect.ValueOf 用于获取反射对象,它们是使用反射进行类型分析和操作的基础。

1.1.3.3 3个判断的方法IsNil IsValid IsZero

在Go语言的reflect包中,reflect.Value类型提供了三个非常有用的方法:IsNilIsValidIsZero

  1. IsNil方法

    IsNil方法用于检查reflect.Value的底层值是否为nil。这通常用于检查指针类型的reflect.Value是否为nil指针。如果reflect.Value包装的值是nilIsNil方法返回true,否则返回false

    示例:

    var x *int
    v := reflect.ValueOf(x)
    fmt.Println("IsNil:", v.IsNil()) // 输出: IsNil: true
    
  2. IsValid方法

    IsValid方法用于检查reflect.Value是否包含一个有效的值。如果reflect.Value是无效的(例如,由于传入了无效的参数导致),IsValid方法返回false,否则返回true

    示例:

    var x int
    v := reflect.ValueOf(x)
    fmt.Println("IsValid:", v.IsValid()) // 输出: IsValid: true
    ​
    var y chan int
    w := reflect.ValueOf(y)
    fmt.Println("IsValid:", w.IsValid()) // 输出: IsValid: false
    
  3. IsZero方法

    IsZero方法用于检查reflect.Value是否为其类型的零值。零值的定义取决于reflect包中对于不同类型的零值定义。如果reflect.Value的值是其类型的零值,IsZero方法返回true,否则返回false

    示例:

    var x int
    v := reflect.ValueOf(x)
    fmt.Println("IsZero:", v.IsZero()) // 输出: IsZero: true
    ​
    var y string
    w := reflect.ValueOf(y)
    fmt.Println("IsZero:", w.IsZero()) // 输出: IsZero: true
    ​
    var z float64
    u := reflect.ValueOf(z)
    fmt.Println("IsZero:", u.IsZero()) // 输出: IsZero: true
    ​
    var a []int
    t := reflect.ValueOf(a)
    fmt.Println("IsZero:", t.IsZero()) // 输出: IsZero: true
    

这三个方法都会允许你在运行时动态的检查reflect.Value的属性,以便在处理未知类型的数据时更加灵活。

1.2 常用标准库OS

1.2.1 fmt包实现格式化I/O.

1.2.1.1 Formatter
package main
​
import (
    "fmt"
    "strings"
)
​
type Person struct {
    Name string
    Age  int
}
​
func (p Person) Format(f fmt.State, c rune) {
    switch c {
    case 'l':
        fmt.Fprint(f, strings.ToLower(p.Name))
    case 'u':
        fmt.Fprint(f, strings.ToUpper(p.Name))
    default:
        fmt.Fprintf(f, "Name: %s, Age: %d", p.Name, p.Age)
    }
}
​
func main() {
    p := Person{Name: "Alice", Age: 30}
    fmt.Printf("Person: %v\n", p)
    fmt.Printf("Person (lowercase name): %l\n", p)
    fmt.Printf("Person (uppercase name): %u\n", p)
}
1.2.1.2 GoStringer

GoString方法用于打印作为操作数传递到%#v格式的值。

package main  
​
import "fmt"  
​
type Person struct {  
   Name string  
   Age  int  
}  
​
func (p Person) GoString() string {  
   return fmt.Sprintf("Person{Name: %q, Age: %d}", p.Name, p.Age)  
}  
​
func main() {  
   p := Person{Name: "Alice", Age: 30}  
   fmt.Printf("%#v\n", p)  
}
1.2.1.3 Stringer
import (
    "fmt"
)
​
type Animal struct {
    Name string
    Age  uint
}
​
func (a Animal) String() string {
    return fmt.Sprintf("%v (%d)", a.Name, a.Age)
}
​
func main() {
    a := Animal{
        Name: "Gopher",
        Age:  2,
    }
    fmt.Println(a)
}

1.2.2 环境变量读写

package main
​
import (
  "fmt"
  "os"
)
​
func init() {
  err := os.Setenv("LogLevel", "Debug")
  if err != nil {
    return
  }
}
​
func main() {
  fmt.Println(os.Getenv("GOROOT"))
  fmt.Println(os.Getenv("LogLevel"))
}

这个代码其实很简单,就是在这个go文件初始化的时候,就设置env环境变量,随后在main函数中去调用。

当然,我们也能看到go给我们配置了一些环境变量,我们可以在终端中输入env自己去看一下。

image-20231028101358994.png

1.2.3 文件读取

package main
​
import (
    "fmt"
    "log"
    "os"
)
​
func main() {
    // Open the file
    filePath := "example.txt"
    file, err := os.Open(filePath)
    if err != nil {
        log.Fatal("Error opening file:", err)
    }
    defer file.Close()
​
    // Read the file data
    fileData := make([]byte, 1024)
    n, err := file.Read(fileData)
    if err != nil {
        log.Fatal("Error reading file:", err)
    }
​
    // Print the file contents
    fmt.Println("File Contents:")
    fmt.Println(string(fileData[:n]))
}

循环读取所有数据

package main  
​
import (  
   "fmt"  
   "io"   "log"   "os")  
​
func main() {  
   // Open the file  
   filePath := "anki.txt"  
   file, err := os.Open(filePath)  
   if err != nil {  
      log.Fatal("Error opening file:", err)  
   }  
   defer file.Close()  
​
   // Read the file data  
   fileData := make([]byte, 1024)  
   for {  
      n, err := file.Read(fileData)  
      if err != nil {  
         if err == io.EOF {  
            break  
         }  
         panic(err)  
      }  
      fmt.Println("File Contents:")  
      fmt.Println(string(fileData[:n]))  
   }  
}

解释一下:

  1. file, err := os.Open("123.txt"): 这行代码尝试打开名为 "123.txt" 的文件。如果文件成功打开,它将返回一个文件对象 filenil 的错误值 err。如果文件打开失败,err 将包含一个描述错误的消息。
  2. if err != nil { panic(err) }: 这行代码检查文件打开的错误。如果发生了错误(err 不为 nil),程序将会进入紧急状态,打印错误信息并终止程序执行。panic 函数用于引发一个运行时错误。
  3. defer file.Close(): 这行代码确保在函数返回之前关闭文件。defer 关键字用于延迟函数的执行,这里是延迟文件的关闭操作。
  4. fileData := make([]byte, 1024): 这行代码创建一个长度为 1024 字节的字节数组,用于存储从文件中读取的数据。
  5. for { ... }: 这是一个无限循环,它会一直执行直到遇到 break 语句为止。在循环内部,程序尝试从文件中读取数据并将其存储到 fileData 字节数组中。
  6. n, err := file.Read(fileData): 这行代码尝试从文件中读取数据,将读取的字节数存储在变量 n 中,并将可能出现的错误存储在变量 err 中。
  7. if err != nil { ... }: 这个条件判断语句检查读取操作是否发生错误。如果出现了错误,程序会根据错误的类型采取不同的行动。如果错误是文件结束错误 (io.EOF),表示已经读取到文件末尾,循环会被中断。否则,如果是其他类型的错误,程序会进入紧急状态并打印错误信息,然后终止执行。
  8. fmt.Println(n): 这行代码打印每次从文件中读取的字节数。这对于了解文件读取的进展非常有用。
  9. fmt.Println(string(fileData[:n])): 这行代码将读取的字节数据转换为字符串并打印出来。注意 fileData[:n] 表示字节数组的前 n 个元素,这是因为 file.Read 可能读取少于 1024 字节,所以我们只打印实际读取的部分。

使用 ioutil.ReadAll 一次性读取

package main  
​
import (  
   "fmt"  
   "io"   "log"   "os")  
​
func main() {  
   // Open the file  
   filePath := "anki.txt"  
   file, err := os.Open(filePath)  
   if err != nil {  
      log.Fatal("Error opening file:", err)  
   }  
   defer file.Close()  
​
   data, err := io.ReadAll(file)  
   if err != nil {  
      log.Fatal("Error reading file:", err)  
   }  
   fmt.Println("File Contents:")  
   fmt.Println(string(data))  
}

使用bufio一行一行读

package main  
​
import (  
   "bufio"  
   "fmt"   "io"   "os")  
​
func main() {  
   file, err := os.Open("example.csv")  
   if err != nil {  
      panic(err)  
   }  
   defer file.Close()  
​
   reader := bufio.NewReader(file)  
   for {  
      line, _, err := reader.ReadLine()  
      if err != nil {  
         if err == io.EOF {  
            break  
         }  
         panic(err)  
      }  
      fmt.Println(string(line))  
   }  
}

bufio 包提供了一个方便的 Reader 类型,用于高效地从输入源(例如文件)中读取数据。reader.ReadLine() 函数用于从 Reader 中读取一行数据。该函数返回三个值:

  1. line []byte: 这个参数代表读取的一行数据。它以字节切片的形式返回,因为Go中的字符串是不可变的,而使用字节切片可以更加灵活地处理文本数据。
  2. prefix bool: 这个参数是一个布尔值,用于指示读取的行是否为前缀行。如果 prefixtrue,说明 linereader.ReadLine() 函数在遇到较长的行时切分的结果。如果 prefixfalse,则表示 line 包含了完整的一行数据。
  3. err error: 这个参数表示在读取过程中是否发生了错误。如果读取成功,err 将为 nil。如果读取到文件末尾,err 将等于 io.EOF。如果在读取过程中发生了其他错误,err 将包含相关的错误信息。

使用 reader.ReadLine() 函数,你可以逐行地从输入源中读取数据,并根据 prefix 参数判断是否需要继续读取下一行以获取完整的文本数据。

1.2.4 os.Open & os.OpenFile

在Go语言中,os.Openos.OpenFile 函数都用于打开文件,但它们之间有一些重要的区别。

1.2.4.1 os.Open 函数

os.Open 函数用于只读方式打开一个文件。它的函数签名如下:

func Open(name string) (*File, error)
  • name 是文件的路径。
  • 返回值是一个指向 File 结构的指针和一个可能的错误。File 结构代表了一个打开的文件。

使用 os.Open 函数,你只能以只读方式打开文件,不能进行写操作。示例代码如下:

file, err := os.Open("example.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()
​
// 从文件中读取数据
1.2.4.2 os.OpenFile 函数

os.OpenFile 函数用于以指定的模式(读、写、追加等)和权限打开文件。它的函数签名如下:

func OpenFile(name string, flag int, perm FileMode) (*File, error)
  • name 是文件的路径。
  • flag 参数用于指定文件的打开模式,可以是 os.O_RDONLY(只读)、os.O_WRONLY(只写)、os.O_RDWR(读写)等。
  • perm 参数用于指定文件的权限,通常用 0666 表示可读写权限。

os.OpenFile 函数允许你在打开文件时指定更多的选项,例如,你可以使用 os.O_APPEND 标志来在文件末尾追加数据,而 os.Open 不提供这样的选项。示例代码如下:

file, err := os.OpenFile("example.txt", os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
    log.Fatal(err)
}
defer file.Close()
​
// 向文件中写入数据

总而言之,如果你只需要以只读方式打开文件,可以使用 os.Open 函数。如果你需要更多的控制,例如指定打开文件的模式、权限等,可以使用 os.OpenFile 函数。

1.2.5 文件写入

package utils
​
import (
  "fmt"
  "os"
)
​
func FileWrite() {
  file, err := os.OpenFile("1231.txt", os.O_WRONLY|os.O_CREATE, 0644)
  if err != nil {
    panic(err)
  }
  defer file.Close()
  for i := 0; i < 50; i++ {
    _, err := file.WriteString(fmt.Sprintf("%d\n", i))
    if err != nil {
      panic(err)
    }
  }
}

这段Go语言代码定义了一个utils包,其中包含一个名为FileWrite的函数。

  1. 导入包

    import (
        "fmt"
        "os"
    )
    

    导入了两个标准库包:fmt用于格式化字符串,os用于处理文件操作。

  2. 函数FileWrite

    func FileWrite() {
    

    函数FileWrite没有任何参数,它的目的是打开或创建一个文件(文件名为"1231.txt"),然后向文件中写入从0到49的整数,每个整数占据一行。

  3. 文件操作

    file, err := os.OpenFile("1231.txt", os.O_WRONLY|os.O_CREATE, 0644)
    if err != nil {
        panic(err)
    }
    defer file.Close()
    

    尝试以写入模式打开文件"1231.txt"。如果文件不存在,它将被创建;如果文件已存在,它将被截断为零长度。os.O_CREATE标志用于在文件不存在时创建文件,os.O_WRONLY标志表示以只写方式打开文件。0644表示文件权限,允许文件所有者读写,其他用户只能读取。

    如果打开文件时发生错误,panic(err)会触发一个恐慌(panic),程序将终止执行并输出错误信息。

    defer file.Close()语句确保在函数返回之前关闭文件,即使在函数内部发生了恐慌。

  4. 写入数据

    for i := 0; i < 50; i++ {
        _, err := file.WriteString(fmt.Sprintf("%d\n", i))
        if err != nil {
            panic(err)
        }
    }
    

    一个循环从0到49,将每个数字转换为字符串并写入文件,每个数字占据一行。file.WriteString函数返回写入的字节数和可能的错误。如果写入时发生错误,panic(err)会触发一个恐慌,程序将终止执行。

总结:创建(如果文件不存在)或截断(如果文件已存在)文件"1231.txt",然后写入从0到49的整数,每个整数占据文件中的一行。如果在文件操作或写入过程中发生错误,程序会进入恐慌状态并终止执行。

1.2.6 文件追加写入

func FileWriteAppend() {
  file, err := os.OpenFile("1234.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
  if err != nil {
    panic(err)
  }
  defer file.Close()
  for i := 0; i < 50; i++ {
    _, err := file.WriteString(fmt.Sprintf("%d\n", i))
    if err != nil {
      panic(err)
    }
  }
}

1.2.7 文件截断

func FileWriteTru() {
  file, err := os.OpenFile("1234.txt", os.O_WRONLY|os.O_TRUNC, 0664)
  if err != nil {
    panic(err)
  }
  defer file.Close()
  for i := 0; i < 10; i++ {
    _, err := file.WriteString(fmt.Sprintf("-%d\n", i))
    if err != nil {
      panic(err)
    }
  }
}

1.2.8 文件截断 seek

func FileSeek() {
  // 写
  file, err := os.OpenFile("1234.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
  if err != nil {
   panic(err)
  }
  defer file.Close()
  for i := 0; i < 10; i++ {
   _, err := file.WriteString(fmt.Sprintf("-%d\n", i))
   if err != nil {
    panic(err)
   }
  }
  // seek
  _, err = file.Seek(0, io.SeekStart)
  if err != nil {
   return
  }
  // 读
  reader := bufio.NewReader(file)
  for {
   line, _, err := reader.ReadLine()
   if err != nil {
    if err == io.EOF {
     break
    }
    panic(err)
   }
   fmt.Println(string(line))
  }
}
  1. 文件的创建和写入:

    file, err := os.OpenFile("1234.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
    

    打开了一个文件,如果文件不存在则创建,如果文件存在则截断文件内容。os.O_RDWR 表示以可读可写的方式打开文件。

    for i := 0; i < 10; i++ {
        _, err := file.WriteString(fmt.Sprintf("-%d\n", i))
        // 写入文件
    }
    

    在一个循环中,它向文件写入了10行数据,每行的内容是 "-数字\n",例如 "-0\n","-1\n" 等。

  2. 文件指针的移动 (Seek):

    _, err = file.Seek(0, io.SeekStart)
    

    将文件指针(即读/写位置)移动到文件的起始位置(偏移量为0)。这是通过使用 file.Seek() 函数来实现的。在这个例子中,io.SeekStart 表示相对于文件的起始位置进行偏移。

  3. 文件的读取:

    reader := bufio.NewReader(file)
    for {
        line, _, err := reader.ReadLine()
        // 读取文件内容
    }
    

    使用 bufio.NewReader() 创建了一个带缓冲的读取器,并通过 ReadLine() 函数逐行读取文件内容。读取的内容被存储在 line 变量中。如果到达文件末尾,ReadLine() 会返回 io.EOF 错误,表示文件结束。在这个例子中,如果读取到文件末尾就会退出循环。

    读取的每一行内容被打印到控制台上:

    fmt.Println(string(line))
    

总而言之,就是是打开一个文件,往文件中写入10行数据,然后将文件指针移动到文件开头,逐行读取文件内容并打印到控制台。

1.2.9 读取目录

func ReadFileContent() {
  file, err := os.OpenFile("a", os.O_RDONLY, 0644)
  if err != nil {
   panic(err)
  }
  defer file.Close()
  dirEntries, err := file.ReadDir(-1)
  if err != nil {
   panic(err)
  }
  for _, entry := range dirEntries {
   info, err := entry.Info()
   if err != nil {
    panic(err)
   }
   fmt.Printf("%v, %v, %v\n", entry.Name(), info.Size(), entry.IsDir())
  }
}

这段代码用于读取目录中的文件信息并将文件名、大小和是否为目录的信息打印到控制台上。以下是代码的详细解释:

  1. 打开目录:

    file, err := os.OpenFile("a", os.O_RDONLY, 0644)
    

    这行代码打开了一个名为 "a" 的目录。os.O_RDONLY 表示以只读的方式打开目录。

  2. 读取目录项:

    dirEntries, err := file.ReadDir(-1)
    

    这行代码使用 ReadDir() 函数读取目录中的所有项。参数 -1 表示读取所有目录项,不进行限制。ReadDir() 函数返回一个 []DirEntry 切片,其中每个 DirEntry 对象包含一个目录项的信息。

  3. 遍历目录项并打印信息:

    for _, entry := range dirEntries {
        info, err := entry.Info()
        // 获取目录项的信息
        if err != nil {
            panic(err)
        }
        fmt.Printf("%v, %v, %v\n", entry.Name(), info.Size(), entry.IsDir())
        // 打印文件名、大小和是否为目录的信息
    }
    

    在循环中,代码遍历 dirEntries 切片,对于每个目录项,通过 entry.Info() 获取详细信息。然后,使用 entry.Name() 获取文件名,info.Size() 获取文件大小,和 entry.IsDir() 判断是否为目录。这些信息被格式化后打印到控制台上。

总的来说,这段代码的目的是打开指定的目录(在这里是名为 "a" 的目录),读取目录中的文件信息,并将文件名、大小和是否为目录的信息打印到控制台上。

1.2.10 递归读取一个目录下面的所有txt文件

// PrintAllTxtFiles 递归读取一个目录下所有的文件
func PrintAllTxtFiles(dir string) error {
  entries, err := os.ReadDir(dir)
  if err != nil {
    return err
  }
  for _, entry := range entries {
    filePath := filepath.Join(dir, entry.Name())
    println(filePath)
    if entry.IsDir() {
      if err := PrintAllTxtFiles(filePath); err != nil {
        return err
      }
    } else if filepath.Ext(filePath) == ".txt" {
      fileBytes, err := os.ReadFile(filePath)
      if err != nil {
        return nil
      }
      fmt.Printf("fileName: %v\n%v\n", filePath, string(fileBytes))
    }
  }
​
  return nil
}
  1. 函数签名:

    func PrintAllTxtFiles(dir string) error {
    

    函数接受一个参数 dir,表示要递归读取的目录的路径。函数的返回类型为 error,表示可能会返回一个错误。

  2. 读取目录项:

    entries, err := os.ReadDir(dir)
    if err != nil {
        return err
    }
    

    使用 os.ReadDir() 函数读取指定目录下的所有目录项。如果读取目录项过程中发生错误,函数会立即返回该错误。

  3. 遍历目录项并处理文件:

    for _, entry := range entries {
        filePath := filepath.Join(dir, entry.Name())
        println(filePath)
        ```
    在循环中,对于每个目录项,构建完整的文件路径 `filePath`,然后将该路径打印到控制台上。
    ​
    ```go
        if entry.IsDir() {
            // 如果是目录,则递归调用 PrintAllTxtFiles 函数
            if err := PrintAllTxtFiles(filePath); err != nil {
                return err
            }
        } else if filepath.Ext(filePath) == ".txt" {
            // 如果是文本文件(扩展名为 .txt),则读取文件内容并打印
            fileBytes, err := os.ReadFile(filePath)
            if err != nil {
                return err
            }
            fmt.Printf("fileName: %v\n%v\n", filePath, string(fileBytes))
        }
    
    • 如果当前目录项是一个子目录,就递归调用 PrintAllTxtFiles() 函数,以处理这个子目录。
    • 如果当前目录项是一个文本文件(文件扩展名为 .txt),则使用 os.ReadFile() 函数读取文件内容,并将文件路径和内容打印到控制台上。
  4. 返回错误:

    return nil
    

    如果所有操作都顺利完成,函数返回 nil 表示没有错误发生。

综上所述,这段代码实现了递归读取指定目录下所有的文件,找到所有扩展名为 .txt 的文本文件,并将文件路径和文件内容打印到控制台上。

1.2.11 使用filepath下的函数简化递归读取流程

WalkDirfilepath 包中提供的一个函数,用于递归地遍历指定目录及其子目录中的所有文件和子目录。WalkDir 函数的签名如下:

func WalkDir(root string, fn func(path string, d fs.DirEntry, err error) error) error
  • root 参数表示要开始遍历的根目录的路径。
  • fn 参数是一个回调函数,用于处理每个访问到的文件和子目录。该回调函数接受三个参数:path 表示当前文件或目录的完整路径,d 是一个实现了 fs.DirEntry 接口的对象,包含了关于当前文件或目录的信息,err 是在获取文件信息时可能发生的错误。

以下是一个使用 WalkDir 函数的例子:

package main
​
import (
  "fmt"
  "io/fs"
  "os"
  "path/filepath"
)
​
func visit(path string, d fs.DirEntry, err error) error {
  if err != nil {
    fmt.Printf("Error accessing path %s: %v\n", path, err)
    return err
  }
  if d.IsDir() {
    fmt.Printf("Directory: %s\n", path)
  } else {
    fmt.Printf("File: %s\n", path)
  }
  return nil
}
​
func main() {
  root := "." // 当前目录
  err := filepath.WalkDir(root, visit)
  if err != nil {
    fmt.Printf("Error walking the path %s: %v\n", root, err)
  }
}

在上面的例子中,visit 函数被传递给 WalkDir 函数作为回调函数。visit 函数会打印出每个访问到的文件和子目录的路径,并指示它们是文件还是目录。filepath.WalkDir 函数会递归遍历当前目录及其子目录,并对每个文件和目录调用 visit 函数。

再举个🌰:

这段代码定义了一个名为 ReadWalkDir 的函数,该函数使用 filepath.WalkDir 函数递归遍历指定目录及其子目录中的所有文件和子目录,并打印所有扩展名为 .txt 的文本文件的路径和文件内容。以下是代码的详细解释:

func ReadWalkDir(dir string) error {
  return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
    fmt.Println(d.IsDir(), "ceshi ~~~~~~~~~~~~~~~~~~~~~~~~")
    if !d.IsDir() && filepath.Ext(d.Name()) == ".txt" {
      fmt.Println(!d.IsDir(), "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
      fileBytes, err := os.ReadFile(path)
      if err != nil {
        return err
      }
      fmt.Printf("filePath: %v\n%v\n", path, string(fileBytes))
    }
    return nil
  })
}
  1. filepath.WalkDir 函数:

    filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
    

    这行代码使用 filepath.WalkDir 函数遍历指定目录 dir 及其子目录中的所有文件和子目录。WalkDir 函数的第二个参数是一个回调函数,该函数会在每次访问到一个文件或目录时被调用。回调函数的参数包括当前文件或目录的完整路径 pathfs.DirEntry 对象 d 包含了关于当前文件或目录的信息,以及可能发生的错误 err

  2. !d.IsDir() && filepath.Ext(d.Name()) == ".txt" 的条件判断:

    if !d.IsDir() && filepath.Ext(d.Name()) == ".txt" {
    

    这个条件判断语句的作用是判断当前访问到的是一个文件(而不是目录),且文件的扩展名是 .txtd.IsDir() 返回 true 表示当前项是一个目录,false 表示是一个文件。filepath.Ext(d.Name()) 返回文件的扩展名,如果扩展名是 .txt,则条件成立。

  3. 处理符合条件的文件:

    fileBytes, err := os.ReadFile(path)
    if err != nil {
        return err
    }
    fmt.Printf("filePath: %v\n%v\n", path, string(fileBytes))
    

    如果当前项是一个符合条件的文本文件,代码使用 os.ReadFile 函数读取文件内容,并将文件路径和文件内容打印到控制台上。

总的来说,这段代码递归地遍历指定目录及其子目录中的所有文件和子目录,在遍历的过程中,判断每个访问到的项是否为文件且扩展名为 .txt,如果是则读取文件内容并将文件路径和内容打印到控制台上。

1.3 单元测试

在Go语言中,单元测试是一种非常重要的软件开发实践,它可以帮助你确保你的代码在各种情况下都能按照预期工作。Go语言的测试框架内建在语言本身中,使得编写和运行单元测试变得非常容易。下面是一个简单的Go语言单元测试的示例:

假设你有一个名为 calculator.go 的文件,其中包含了一个简单的加法函数 Add

// calculator.go
package calculator
​
func Add(a, b int) int {
    return a + b
}

然后,你可以为这个函数编写单元测试。在同一个目录下创建一个名为 calculator_test.go 的文件,用于编写测试代码:

// calculator_test.go
package calculator
​
import "testing"
​
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Add(2, 3) returned %d, expected %d", result, expected)
    }
}

在这个测试文件中,我们使用了 testing 包来编写测试函数 TestAdd。在这个函数中,我们调用了 Add 函数,并使用 t.Errorf 来比较实际的结果和预期的结果。如果两者不相等,测试将会失败。

要运行这个测试,你可以使用 go test 命令。在命令行中,进入包含 calculator.gocalculator_test.go 的目录,然后运行以下命令:

go test

如果一切正常,你应该会看到类似下面的输出:

ok      command-line-arguments  0.001s

这表示你的测试通过了!

1.3.1 库

github.com/stretchr/testify/assert 是一个非常流行的Go语言测试断言库,它提供了丰富的断言函数,用于简化测试代码的编写。使用这个库,你可以更容易地编写清晰、易读的测试代码。下面是一个示例,演示了如何使用 assert 包进行单元测试。

首先,你需要确保你的Go语言环境已经安装了 github.com/stretchr/testify/assert 包。如果没有安装,你可以使用以下命令安装:

go get -u github.com/stretchr/testify/assert

接下来,让我们修改之前的示例代码,使用 assert 包进行单元测试。我们将继续使用之前的 calculator.go 文件和 calculator_test.go 文件,但是这次我们将使用 assert 包的断言函数来编写测试:

// calculator_test.go
package calculator
​
import (
    "testing"
    "github.com/stretchr/testify/assert"
)
​
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    assert.Equal(t, expected, result, "Add(2, 3) should return 5")
}

在这个示例中,我们使用了 assert.Equal 函数来比较实际结果和预期结果。如果它们不相等,assert 包会自动输出详细的错误信息,包括实际值和期望值。这样,你就无需手动编写错误消息了,使得测试代码更加简洁和易读。

要运行这个测试,你仍然可以使用 go test 命令。确保你的当前目录中包含了 calculator.gocalculator_test.go 文件,然后运行以下命令:

go test

如果测试通过,你会看到类似下面的输出:

ok      command-line-arguments  0.001s

这表示你的测试通过了,而且你可以信心满满地知道你的 Add 函数在给定的输入下按照预期工作。

1.4 json序列化和反序列化

在Go语言中,你可以使用encoding/json包来进行JSON序列化(将Go数据结构转换为JSON字符串)和反序列化(将JSON字符串转换为Go数据结构)。以下是JSON序列化和反序列化的基本用法示例:

1.4.1 JSON序列化

package main
​
import (
  "encoding/json"
  "fmt"
)
​
type Person struct {
  Name  string `json:"name"`
  Age   int    `json:"age"`
  City  string `json:"city"`
}
​
func main() {
  // 创建一个Person对象
  person := Person{
    Name: "Alice",
    Age:  30,
    City: "New York",
  }
​
  // 将Person对象序列化为JSON字符串
  jsonData, err := json.Marshal(person)
  // 缩进的
  jsonData, err := json.MarshalIndent(person, "", "    ")
  if err != nil {
    fmt.Println("JSON serialization error:", err)
    return
  }
​
  // 输出JSON字符串
  fmt.Println(string(jsonData))
}

在上面的示例中,我们定义了一个Person结构体,然后将其序列化为JSON字符串。输出结果会是一个包含nameagecity字段的JSON对象。

1.4.2 JSON反序列化

package main
​
import (
  "encoding/json"
  "fmt"
)
​
type Person struct {
  Name  string `json:"name"`
  Age   int    `json:"age"`
  City  string `json:"city"`
}
​
func main() {
  // JSON字符串
  jsonData := `{"name":"Bob","age":25,"city":"London"}`
​
  // 将JSON字符串反序列化为Person对象
  var person Person
  err := json.Unmarshal([]byte(jsonData), &person)
  if err != nil {
    fmt.Println("JSON deserialization error:", err)
    return
  }
​
  // 输出反序列化后的Person对象
  fmt.Println(person)
}

在上面的示例中,我们有一个包含JSON数据的字符串。我们使用json.Unmarshal函数将JSON字符串解析为Person对象。注意,传递给json.Unmarshal函数的第二个参数是一个指向目标结构体的指针,以便函数可以填充结构体的字段。


在Go语言中,JSON编码和解码可以使用encoding/json包中的EncoderDecoder类型来实现。这两个类型提供了更灵活的方式来处理JSON数据流,特别是当你需要处理大量JSON数据时。

1.4.3 使用Encoder进行JSON编码

package main
​
import (
  "encoding/json"
  "fmt"
  "os"
)
​
type Person struct {
  Name  string `json:"name"`
  Age   int    `json:"age"`
  City  string `json:"city"`
}
​
func main() {
  // 创建一个Person对象
  person := Person{
    Name: "Alice",
    Age:  30,
    City: "New York",
  }
​
  // 创建一个文件用于写入JSON数据
  file, err := os.Create("person.json")
  if err != nil {
    fmt.Println("Error creating file:", err)
    return
  }
  defer file.Close()
​
  // 创建JSON编码器
  encoder := json.NewEncoder(file)
​
  // 使用Encoder将Person对象编码为JSON并写入文件
  err = encoder.Encode(person)
  if err != nil {
    fmt.Println("JSON encoding error:", err)
    return
  }
​
  fmt.Println("JSON data has been encoded and written to person.json")
}

在这个示例中,我们创建了一个Person对象,并将其使用json.NewEncoder函数创建的Encoder对象编码并写入一个文件中。

1.4.4 使用Decoder进行JSON解码

package main
​
import (
  "encoding/json"
  "fmt"
  "os"
)
​
type Person struct {
  Name  string `json:"name"`
  Age   int    `json:"age"`
  City  string `json:"city"`
}
​
func main() {
  // 打开包含JSON数据的文件
  file, err := os.Open("person.json")
  if err != nil {
    fmt.Println("Error opening file:", err)
    return
  }
  defer file.Close()
​
  // 创建JSON解码器
  decoder := json.NewDecoder(file)
​
  // 创建一个空的Person对象,用于存储解码后的JSON数据
  var person Person
​
  // 使用Decoder将JSON数据解码到Person对象中
  err = decoder.Decode(&person)
  if err != nil {
    fmt.Println("JSON decoding error:", err)
    return
  }
​
  // 输出解码后的Person对象
  fmt.Println("Decoded Person:", person)
}

在这个示例中,我们打开包含JSON数据的文件,然后使用json.NewDecoder函数创建Decoder对象。我们创建了一个空的Person对象,然后使用Decode方法将JSON数据解码到Person对象中。