跟着产品自学Go - 4_进阶用法

1.1 反射reflect

1.1.1 调试工具 dlv

GitHub:

github.com/go-delve/de...

下载方式:

bash 复制代码
go install github.com/go-delve/delve/cmd/dlv@latest

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

Windows:

learn.microsoft.com/zh-cn/power...

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

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

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

  • 第一步

    bash 复制代码
    dlv debug main.go

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

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

Type 复制代码
(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:指向实现了接口方法的具体值的指针。

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

go 复制代码
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
}
yaml 复制代码
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:指向实际值的指针。

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

go 复制代码
package main

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

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

    yaml 复制代码
    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 类型的对象。这个函数的签名如下:

go 复制代码
func TypeOf(i interface{}) Type

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

go 复制代码
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:这将返回类型的名称。切片或指针,没有名称,此方法返回一个空字符串。

go 复制代码
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())
}
1.1.3.1.2 TypeOf 函数常用方法--Kind
go 复制代码
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)
}
1.1.3.1.3 TypeOf 函数常用方法--Elem

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

go 复制代码
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())
}

举两个🌰

go 复制代码
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 方法获得了字段的元素类型。

go 复制代码
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 值,表示结构体中的字段数量。以下是一个示例:

go 复制代码
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 类型的对象,其中包含了字段的详细信息,比如字段的名称、类型等。以下是一个示例:

v 复制代码
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.举个🌰

go 复制代码
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 类型的对象。这个函数的签名如下:

go 复制代码
func ValueOf(i interface{}) Value

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

go 复制代码
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 函数示例
go 复制代码
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 包中的函数进行操作:

go 复制代码
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: "alice@example.com",
  }
​
  // 调用处理结构体的函数
  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

    示例:

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

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

    示例:

    go 复制代码
    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

    示例:

    go 复制代码
    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
go 复制代码
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格式的值。

go 复制代码
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
go 复制代码
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 环境变量读写

go 复制代码
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自己去看一下。

1.2.3 文件读取

go 复制代码
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]))
}

循环读取所有数据

go 复制代码
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 一次性读取

go 复制代码
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一行一行读

go 复制代码
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 函数用于只读方式打开一个文件。它的函数签名如下:

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

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

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

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

go 复制代码
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 不提供这样的选项。示例代码如下:

go 复制代码
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 文件写入

go 复制代码
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. 导入包

    go 复制代码
    import (
        "fmt"
        "os"
    )

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

  2. 函数FileWrite

    go 复制代码
    func FileWrite() {

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

  3. 文件操作

    go 复制代码
    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. 写入数据

    go 复制代码
    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 文件追加写入

go 复制代码
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 文件截断

go 复制代码
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

go 复制代码
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. 文件的创建和写入:

    go 复制代码
    file, err := os.OpenFile("1234.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)

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

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

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

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

    go 复制代码
    _, err = file.Seek(0, io.SeekStart)

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

  3. 文件的读取:

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

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

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

    go 复制代码
    fmt.Println(string(line))

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

1.2.9 读取目录

go 复制代码
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. 打开目录:

    go 复制代码
    file, err := os.OpenFile("a", os.O_RDONLY, 0644)

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

  2. 读取目录项:

    go 复制代码
    dirEntries, err := file.ReadDir(-1)

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

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

    go 复制代码
    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文件

go 复制代码
// 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. 函数签名:

    go 复制代码
    func PrintAllTxtFiles(dir string) error {

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

  2. 读取目录项:

    go 复制代码
    entries, err := os.ReadDir(dir)
    if err != nil {
        return err
    }

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

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

    go 复制代码
    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. 返回错误:

    go 复制代码
    return nil

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

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

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

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

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

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

go 复制代码
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 的文本文件的路径和文件内容。以下是代码的详细解释:

go 复制代码
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 函数:

    go 复制代码
    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" 的条件判断:

    go 复制代码
    if !d.IsDir() && filepath.Ext(d.Name()) == ".txt" {

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

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

    go 复制代码
    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

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

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

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 复制代码
go test

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

go 复制代码
ok      command-line-arguments  0.001s

这表示你的测试通过了!

1.3.1 库

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

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

go 复制代码
go get -u github.com/stretchr/testify/assert

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

go 复制代码
// 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 复制代码
go test

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

go 复制代码
ok      command-line-arguments  0.001s

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

1.4 json序列化和反序列化

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

1.4.1 JSON序列化

go 复制代码
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反序列化

go 复制代码
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编码

go 复制代码
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解码

go 复制代码
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对象中。

相关推荐
sunbin4 分钟前
稀土掘金我要吐槽你
后端
程序员鱼皮2 小时前
我代表编程导航,向大家道歉!
前端·后端·程序员
zjjuejin2 小时前
Maven 生命周期与插件机制
后端·maven
阿杆2 小时前
为什么我建议你把自建 Redis 迁移到云上进行托管
redis·后端
Java水解2 小时前
go语言教程(全网最全,持续更新补全)
后端·go
bobz9652 小时前
QEMU 使用 DPDK 时候在 libvirt xml 中设置 sock 的目的
后端
thinktik2 小时前
AWS EKS 计算资源自动扩缩之按需申请Fargate[AWS 中国宁夏区]
后端·aws
thinktik3 小时前
AWS EKS 实现底层EC2计算资源的自动扩缩[AWS 中国宁夏区]
后端·aws
uhakadotcom3 小时前
什么是OpenTelemetry?
后端·面试·github
知其然亦知其所以然3 小时前
MySQL 社招必考题:如何优化特定类型的查询语句?
后端·mysql·面试