Go语言内置函数、标准库、常用工具命令

本文主要讲解了GO的内置如len()、cap()、make()等以及常用的标准库:io、http等,还有go常用的命令go get、go install等等,篇幅较长,可搜查参考。

欢迎关注一起交流!

 Go语言内置函数

Go语言的标准库中包含了许多内置函数,这些函数可以直接在代码中使用,无需导入任何包。下面列举了一些常用的Go语言内置函数,并附上了它们的基本用法和示例代码。

1. len()

len() 函数用于获取数组、切片、字符串、映射、通道的长度,或者集合中元素的数量。

语法

func len(v Type) int

含义

  • v Type:要获取长度的对象,可以是数组、切片、字符串、映射或通道。

  • int:返回对象的长度。

示例代码:

package main  

import "fmt"  

func main() {  
    s := "Hello, Go!"  
    fmt.Println(len(s)) // 输出:10  

    slice := []int{1, 2, 3, 4, 5}  
    fmt.Println(len(slice)) // 输出:5  

    m := map[string]int{"a": 1, "b": 2}  
    fmt.Println(len(m)) // 输出:2  
}

2. cap()

cap() 函数用于获取切片或通道的容量。

语法

go复制代码func cap(v Type) int

含义

  • v Type:要获取容量的对象,通常是切片或通道。

  • int:返回对象的容量。

示例代码:

package main  

import "fmt"  

func main() {  
    slice := make([]int, 5, 10) // 切片长度为5,容量为10  
    fmt.Println(cap(slice)) // 输出:10  
}

3. new()

new() 函数用于分配内存并返回指向该类型的零值的指针。

语法:

func new(Type) *Type

含义

  • Type:要分配的类型。

  • *Type:返回指向新分配类型零值的指针。

示例代码:

package main  

import "fmt"  

type Person struct {  
    Name string  
    Age  int  
}  

func main() {  
    p := new(Person)  
    fmt.Println(p) // 输出:&{ 0}  
}

4. make()

make() 函数用于初始化切片、映射和通道,并返回初始化后的对象(非指针)。

语法:

func make(t Type, size ...IntegerType) Type

含义

  • t Type:指定要创建的类型,通常是切片、映射或通道。

  • size ...IntegerType:可变参数,指定类型的大小。对于切片,它指定了切片的长度和容量(可选);对于映射和通道,它通常不使用。

  • Type:返回新创建的类型的对象。

示例代码:

package main  

import "fmt"  

func main() {  
    slice := make([]int, 5)  
    fmt.Println(slice) // 输出:[0 0 0 0 0]  

    m := make(map[string]int)  
    fmt.Println(m) // 输出:map[]  

    ch := make(chan int)  
    // 注意:通道的发送和接收操作在这里省略了  
}

5. append()

append() 函数用于向切片追加一个或多个元素,并返回新的切片。

语法:

func append(slice []Type, elems ...Type) []Type

含义

  • slice []Type:要追加元素的切片。

  • elems ...Type:可变参数,要追加到切片的元素。

  • []Type:返回新的切片,包含原始切片和追加的元素。

示例代码:

package main  

import "fmt"  

func main() {  
    slice := []int{1, 2, 3}  
    slice = append(slice, 4)  
    fmt.Println(slice) // 输出:[1 2 3 4]  

    slice = append(slice, 5, 6)  
    fmt.Println(slice) // 输出:[1 2 3 4 5 6]  
}

6. copy()

copy() 函数用于将一个切片的内容复制到另一个切片中。

语法:

func copy(dst, src []Type) int

含义

  • dst []Type:目标切片,用于接收复制的元素。

  • src []Type:源切片,包含要复制的元素。

  • int:返回复制的元素数量,通常是min(len(dst), len(src))

示例代码:

package main  

import "fmt"  

func main() {  
    src := []int{1, 2, 3, 4, 5}  
    dst := make([]int, 3)  
    n := copy(dst, src)  
    fmt.Println(dst) // 输出:[1 2 3]  
    fmt.Println(n)   // 输出:3  
}

7. close()

close() 函数用于关闭通道,表示通道中没有更多的数据会被发送。

语法:

func close(ch chan<- Type)

含义:

  • ch chan<- Type:表示一个只能发送数据到通道ch的通道。Type是通道中传输的数据类型。

示例代码:

package main  

import "fmt"  

func main() {  
    ch := make(chan int)  
    go func() {  
        ch <- 42  
        close(ch)  
    }()  

    value, ok := <-ch  
    fmt.Println(value) // 输出:42  
    fmt.Println(ok)    // 输出:true  

    value, ok = <-ch  
    fmt.Println(value) // 输出:0  
    fmt.Println(ok)    // 输出:false,表示通道已关闭且没有接收到新的值  
}

8. delete()

delete() 函数用于从映射中删除键值对。

语法:

func delete(m map[Type]Type, key Type)

含义

  • m map[Type]Type:要从中删除键的映射。

  • key Type:要删除的键。

  • 该函数没有返回值,用于从映射中删除指定的键值对。

示例代码:

package main  

import "fmt"  

func main() {  
    m := map[string]int{"a": 1, "b": 2}  
    delete(m, "a")  
    fmt.Println(m) // 输出:map[b:2]  
}

9. panic() 和 recover

panic() 和 recover() 是 Go 语言中用于处理运行时错误和异常的两个内置函数。panic()

panic() 函数用于触发一个运行时错误。当函数 panic 被调用时,当前的函数执行会立即停止,然后逐层向上执行所有已延迟的函数(通过 defer 关键字声明的),直到当前 goroutine 的栈被完全展开。如果 panic 的调用没有在任何地方被 recover 捕获,程序会崩溃并输出错误信息。

语法:

func panic(v interface{})

含义

  • v interface{}:传递给panic的值,可以是任何类型。

  • 该函数没有返回值,用于触发运行时错误。

示例代码:

package main  

import "fmt"  

func main() {  
    defer func() {  
        if r := recover(); r != nil {  
            fmt.Println("Recovered in main", r)  
        }  
    }()  
    foo()  
}  

func foo() {  
    bar()  
}  

func bar() {  
    panic("something bad happened")  
}

在这个例子中,bar() 函数中的 panic 调用会触发一个运行时错误。由于 main() 函数中使用了 defer 和 recover(),这个错误会被捕获,并输出 "Recovered in main something bad happened"。

recover()

recover() 函数用于在 defer 函数中恢复由 panic 触发的错误。它必须在一个 defer 函数中直接调用,否则无法正常工作。如果当前的 goroutine 没有发生 panicrecover() 会返回 nil

语法:

func recover() interface{}

含义

  • interface{}:返回最近一次panic调用传递的值,如果没有panic发生,则返回nil

  • 该函数用于在defer函数中恢复panic引发的错误。

示例代码:

package main  

import "fmt"  

func main() {  
    defer func() {  
        if r := recover(); r != nil {  
            fmt.Println("Recovered in main", r)  
        }  
    }()  
    foo()  
    fmt.Println("Continuing after recovery")  
}  

func foo() {  
    bar()  
}  

func bar() {  
    panic("something bad happened")  
}

在这个例子中,recover() 在 main() 函数的 defer 函数中被调用,成功捕获了 bar() 函数中 panic 触发的错误。因此,尽管有 panic 发生,程序依然会打印出 "Continuing after recovery"。

注意事项

  • panic 和 recover 主要用于处理不可恢复的错误或异常情况,而不是用于普通的错误处理。对于可预期的错误,应使用返回值和错误变量。

  • recover() 只能在 defer 函数中调用,且只有在同一个 goroutine 中发生 panic 时才有效。

  • 在没有 panic 发生的情况下,recover() 会返回 nil

  • 不应该随意在程序中引发 panic,因为它会中断当前 goroutine 的执行。在可能的情况下,应优先考虑使用返回值和错误变量来传递错误信息。

10. print()printf() 和 println()

print() 和 println() 函数用于在标准输出中打印信息。print() 不会在输出后自动换行,而 println() 则会在输出后自动换行,printf() 用于格式化输出。

println()语法:

func Println(a ...interface{}) (n int, err error)

含义

  • a ...interface{}:可变参数,接受任意数量和类型的参数。

  • n int:返回打印的字节数。

  • err error:在发生错误时返回错误信息,通常用于I/O错误,但Println通常不会返回错误。

Println函数会自动在每个参数后面添加一个空格,并在最后添加一个换行符。

print 语法:

func Print(a ...interface{}) (n int, err error)

含义

  • a ...interface{}:可变参数,接受任意数量和类型的参数。

  • n int:返回打印的字节数。

  • err error:在发生错误时返回错误信息。

Print函数与Println类似,但它不会在最后添加换行符。

printf() 语法:

func Printf(format string, a ...interface{}) (n int, err error)

含义

  • format string:格式化字符串,它定义了如何格式化后面的参数。

  • a ...interface{}:可变参数,与format字符串中的占位符相对应。

  • n int:返回打印的字节数。

  • err error:在发生错误时返回错误信息。

Printf函数使用format字符串中的占位符来格式化参数,并输出结果。占位符通常以%开头,后面跟着一个或多个字符来指定格式。

示例代码:

package main  

import "fmt"  

func main() {  
    fmt.Print("Hello", "World") // 输出:Hello World(后面不会跟换行符) 
    fmt.Println("Hello", "World") // 输出:Hello World(后面跟着一个换行符) 
    fmt.Printf("Hello, %s! You have %d messages.\n", "Alice", 5) // 输出:Hello, Alice! You have 5 messages.(后面跟着一个换行符)
}

11. complex()real(), 和 imag()

complex() 函数用于生成一个复数,real() 和 imag() 函数分别用于获取复数的实部和虚部。

complex()语法:

func complex(r, i floatType) complexType

含义

  • r:复数的实部。

  • i:复数的虚部。

  • floatType:实部和虚部的类型,必须是float32float64

  • complexType:返回值的类型,对应于实部和虚部的类型,即complex64complex128

complex()函数用于创建一个复数,其中r是复数的实部,i是复数的虚部。

real() 语法:

func real(c complexType) floatType

含义

  • c:一个复数。

  • complexType:复数的类型,即complex64complex128

  • floatType:返回值的类型,对应于复数的实部类型,即float32float64

real()函数用于获取复数的实部。

imag() 语法:

func imag(c complexType) floatType

含义

  • c:一个复数。

  • complexType:复数的类型,即complex64complex128

  • floatType:返回值的类型,对应于复数的虚部类型,即float32float64

imag()函数用于获取复数的虚部。

示例代码:

package main  

import (  
    "fmt"  
    "math/cmplx"  
)  

func main() {  
    c := complex(3, 4)  
    fmt.Println(c)            // 输出:(3+4i)  
    fmt.Println(real(c))     // 输出:3  
    fmt.Println(imag(c))     // 输出:4  
    fmt.Println(cmplx.Abs(c)) // 输出:5,计算复数的模(绝对值)  
}

注意事项

  • 内置函数是Go语言标准库的一部分,因此无需导入任何包即可直接使用。

  • 内置函数的设计初衷是为了提供一些基础的、常用的功能,以简化编程任务。

  • 虽然内置函数提供了方便,但在实际编程中,还需要根据具体需求选择使用内置函数还是其他库函数。

Go语言的内置函数集合是固定的,并且涵盖了大多数常见的编程需求。然而,随着Go语言的不断发展和更新,可能会有新的内置函数被添加,或者现有的内置函数的行为可能会有所改变。因此,建议在使用内置函数时查阅最新的Go语言文档以获取最准确的信息。

2 常用标准库

2.1 fmt

fmt包实现了类似C语言printf和scanf的格式化I/O。主要分为向外输出内容和获取输入内容两大部分。

2.1.1 向外输出

标准库fmt提供了以下几种输出相关函数。

Print

Print系列函数会将内容输出到系统的标准输出,区别在于Print函数直接输出内容,Printf函数支持格式化输出字符串,Println函数会在输出内容的结尾添加一个换行符。

func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

举个简单的例子:

func main() {
    fmt.Print("在终端打印该信息。")
    name := "阿尘"
    fmt.Printf("我是:%s\n", name)
    fmt.Println("在终端打印单独一行显示")
}

执行上面的代码输出:

在终端打印该信息。我是:阿尘
    在终端打印单独一行显示
Fprint

Fprint系列函数会将内容输出到一个io.Writer接口类型的变量w中,我们通常用这个函数往文件中写入内容。

func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)

举个例子:

// 向标准输出写入内容
fmt.Fprintln(os.Stdout, "向标准输出写入内容")
fileObj, err := os.OpenFile("./xx.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
    fmt.Println("打开文件出错,err:", err)
    return
}
name := "阿尘"
// 向打开的文件句柄中写入内容
fmt.Fprintf(fileObj, "往文件中写如信息:%s", name)

注意,只要满足io.Writer接口的类型都支持写入。

Sprint

Sprint系列函数会把传入的数据生成并返回一个字符串。

func Sprint(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string
func Sprintln(a ...interface{}) string

简单的示例代码如下:

s1 := fmt.Sprint("阿尘")
name := "阿尘"
age := 28
s2 := fmt.Sprintf("name:%s,age:%d", name, age)
s3 := fmt.Sprintln("阿尘")
fmt.Println(s1, s2, s3)
Errorf

Errorf函数根据format参数生成格式化字符串并返回一个包含该字符串的错误。

func Errorf(format string, a ...interface{}) error

通常使用这种方式来自定义错误类型,例如:

err := fmt.Errorf("这是一个错误")
2.1.2. 格式化占位符

*printf系列函数都支持format格式化参数,在这里我们按照占位符将被替换的变量类型划分,方便查询和记忆。

通用占位符
占位符 说明
%v 值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名
%#v 值的Go语法表示
%T 打印值的类型
%% 百分号

示例代码如下:

fmt.Printf("%v\n", 100)
fmt.Printf("%v\n", false)
o := struct{ name string }{"阿尘"}
fmt.Printf("%v\n", o)
fmt.Printf("%#v\n", o)
fmt.Printf("%T\n", o)
fmt.Printf("100%%\n")

输出结果如下:

100
false
{阿尘}
struct { name string }{name:"阿尘"}
struct { name string }
100%
布尔型
占位符 说明
%t true或false
整型
占位符 说明
%b 表示为二进制
%c 该值对应的unicode码值
%d 表示为十进制
%o 表示为八进制
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于”U+%04X”
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示

示例代码如下:

n := 65
fmt.Printf("%b\n", n)
fmt.Printf("%c\n", n)
fmt.Printf("%d\n", n)
fmt.Printf("%o\n", n)
fmt.Printf("%x\n", n)
fmt.Printf("%X\n", n)

输出结果如下:

1000001
    A
    65
    101
    41
    41
浮点数与复数
占位符 说明
%b 无小数部分、二进制指数的科学计数法,如-123456p-78
%e 科学计数法,如-1234.456e+78
%E 科学计数法,如-1234.456E+78
%f 有小数部分但无指数部分,如123.456
%F 等价于%f
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)

示例代码如下:

f := 12.34
fmt.Printf("%b\n", f)
fmt.Printf("%e\n", f)
fmt.Printf("%E\n", f)
fmt.Printf("%f\n", f)
fmt.Printf("%g\n", f)
fmt.Printf("%G\n", f)

输出结果如下:

6946802425228990p-49
    1.234000e+01
    1.234000E+01
    12.340000
    12.34
    12.34
字符串和[]byte
占位符 说明
%s 直接输出字符串或者[]byte
%q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
%x 每个字节用两字符十六进制数表示(使用a-f
%X 每个字节用两字符十六进制数表示(使用A-F)

示例代码如下:

s := "阿尘"
    fmt.Printf("%s\n", s)
    fmt.Printf("%q\n", s)
    fmt.Printf("%x\n", s)
    fmt.Printf("%X\n", s)

输出结果如下:

阿尘
    "阿尘"
    e69eafe897a4
    E69EAFE897A4
指针
占位符 说明
%p 表示为十六进制,并加上前导的0x

示例代码如下:

a := 28
fmt.Printf("%p\n", &a)
fmt.Printf("%#p\n", &a)

输出结果如下:

0xc00000a140
c00000a140
宽度标识符

宽度通过一个紧跟在百分号后面的十进制数指定,如果未指定宽度,则表示值时除必需之外不作填充。精度通过(可选的)宽度后跟点号后跟的十进制数指定。如果未指定精度,会使用默认精度;如果点号后没有跟数字,表示精度为0。举例如下

占位符 说明
%f 默认宽度,默认精度
%9f 宽度9,默认精度
%.2f 默认宽度,精度2
%9.2f 宽度9,精度2
%9.f 宽度9,精度0

示例代码如下:

n := 88.88
fmt.Printf("%f\n", n)
fmt.Printf("%9f\n", n)
fmt.Printf("%.2f\n", n)
fmt.Printf("%9.2f\n", n)
fmt.Printf("%9.f\n", n)

输出结果如下:

88.880000
    88.880000
    88.88
        88.88
           89
其他falg
占位符 说明
’+’ 总是输出数值的正负号;对%q(%+q)会生成全部是ASCII字符的输出(通过转义);
’ ‘ 对数值,正数前加空格而负数前加负号;对字符串采用%x或%X时(% x或% X)会给各打印的字节之间加空格
’-’ 在输出右边填充空白而不是默认的左边(即从默认的右对齐切换为左对齐);
’#’ 八进制数前加0(%#o),十六进制数前加0x(%#x)或0X(%#X),指针去掉前面的0x(%#p)对%q(%#q),对%U(%#U)会输出空格和单引号括起来的go字面值;
‘0’ 使用0而不是空格填充,对于数值类型会把填充的0放在正负号后面;

举个例子:

s := "阿尘"
fmt.Printf("%s\n", s)
fmt.Printf("%5s\n", s)
fmt.Printf("%-5s\n", s)
fmt.Printf("%5.7s\n", s)
fmt.Printf("%-5.7s\n", s)
fmt.Printf("%5.2s\n", s)
fmt.Printf("%05s\n", s)

输出结果如下:

阿尘
       阿尘
    阿尘
       阿尘
    阿尘
       阿尘
    000阿尘
2.1.3. 获取输入

Go语言fmt包下有fmt.Scan、fmt.Scanf、fmt.Scanln三个函数,可以在程序运行过程中从标准输入获取用户的输入。

fmt.Scan

函数定签名如下:

func Scan(a ...interface{}) (n int, err error)
  • Scan从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符。

  • 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因。

具体代码示例如下:

func main() {
    var (
        name    string
        age     int
        married bool
    )
    fmt.Scan(&name, &age, &married)
    fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

将上面的代码编译后在终端执行,在终端依次输入阿尘、28和false使用空格分隔。

$ ./scan_demo 
    阿尘 28 false
    扫描结果 name:阿尘 age:28 married:false

fmt.Scan从标准输入中扫描用户输入的数据,将以空白符分隔的数据分别存入指定的参数。

fmt.Scanf

函数签名如下:

func Scanf(format string, a ...interface{}) (n int, err error)
  • Scanf从标准输入扫描文本,根据format参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中。

  • 本函数返回成功扫描的数据个数和遇到的任何错误。

代码示例如下:

func main() {
    var (
        name    string
        age     int
        married bool
    )
    fmt.Scanf("1:%s 2:%d 3:%t", &name, &age, &married)
    fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

将上面的代码编译后在终端执行,在终端按照指定的格式依次输入阿尘、28和false。

$ ./scan_demo 
    1:阿尘 2:28 3:false
    扫描结果 name:阿尘 age:28 married:false

fmt.Scanf不同于fmt.Scan简单的以空格作为输入数据的分隔符,fmt.Scanf为输入数据指定了具体的输入内容格式,只有按照格式输入数据才会被扫描并存入对应变量。

例如,我们还是按照上个示例中以空格分隔的方式输入,fmt.Scanf就不能正确扫描到输入的数据。

$ ./scan_demo 
    阿尘 28 false
    扫描结果 name: age:0 married:false
fmt.Scanln

函数签名如下:

func Scanln(a ...interface{}) (n int, err error)
  • Scanln类似Scan,它在遇到换行时才停止扫描。最后一个数据后面必须有换行或者到达结束位置。

  • 本函数返回成功扫描的数据个数和遇到的任何错误。

具体代码示例如下:

func main() {
        var (
            name    string
            age     int
            married bool
        )
        fmt.Scanln(&name, &age, &married)
        fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
    }

将上面的代码编译后在终端执行,在终端依次输入阿尘、28和false使用空格分隔。

$ ./scan_demo 
    阿尘 28 false
    扫描结果 name:阿尘 age:28 married:false

fmt.Scanln遇到回车就结束扫描了,这个比较常用。

bufio.NewReader

有时候我们想完整获取输入的内容,而输入的内容可能包含空格,这种情况下可以使用bufio包来实现。示例代码如下:

func bufioDemo() {
    reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
    fmt.Print("请输入内容:")
    text, _ := reader.ReadString('\n') // 读到换行
    text = strings.TrimSpace(text)
    fmt.Printf("%#v\n", text)
}
Fscan系列

这几个函数功能分别类似于fmt.Scan、fmt.Scanf、fmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从io.Reader中读取数据。

func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)
Sscan系列

这几个函数功能分别类似于fmt.Scan、fmt.Scanf、fmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从指定字符串中读取数据。

func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)

2.2 Time

时间和日期是我们编程中经常会用到的,这里简单介绍Go语言内置的time包的基本用法。

2.2.1. time包

time包提供了时间的显示和测量用的函数。日历的计算采用的是公历。

2.2.2. 时间类型

time.Time类型表示时间。我们可以通过time.Now()函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息。示例代码如下:

func timeDemo() {
    now := time.Now() //获取当前时间
    fmt.Printf("current time:%v\n", now)

    year := now.Year()     //年
    month := now.Month()   //月
    day := now.Day()       //日
    hour := now.Hour()     //小时
    minute := now.Minute() //分钟
    second := now.Second() //秒
    fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}
2.2.3. 时间戳

时间戳是自1970年1月1日(08:00:00GMT)至当前时间的总毫秒数。它也被称为Unix时间戳(UnixTimestamp)。

基于时间对象获取时间戳的示例代码如下:

func timestampDemo() {
    now := time.Now()            //获取当前时间
    timestamp1 := now.Unix()     //时间戳
    timestamp2 := now.UnixNano() //纳秒时间戳
    fmt.Printf("current timestamp1:%v\n", timestamp1)
    fmt.Printf("current timestamp2:%v\n", timestamp2)
}

使用time.Unix()函数可以将时间戳转为时间格式。

func timestampDemo2(timestamp int64) {
    timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式
    fmt.Println(timeObj)
    year := timeObj.Year()     //年
    month := timeObj.Month()   //月
    day := timeObj.Day()       //日
    hour := timeObj.Hour()     //小时
    minute := timeObj.Minute() //分钟
    second := timeObj.Second() //秒
    fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}
2.2.4. 时间间隔

time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。time.Duration表示一段时间间隔,可表示的最长时间段大约290年。

time包中定义的时间间隔类型的常量如下:

const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)

例如:time.Duration表示1纳秒,time.Second表示1秒。

2.2.5. 时间操作
Add

我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求,Go语言的时间对象有提供Add方法如下:

func (t Time) Add(d Duration) Time

举个例子,求一个小时之后的时间:

func main() {
    now := time.Now()
    later := now.Add(time.Hour) // 当前时间加1小时后的时间
    fmt.Println(later)
}
Sub

求两个时间之间的差值:

返回一个时间段t-u。如果结果超出了Duration可以表示的最大值/最小值,将返回最大值/最小值。要获取时间点t-d(d为Duration),可以使用t.Add(-d)。

package main  

import (  
    "fmt"  
    "time"  
)  

func main() {  
    // 创建两个时间对象  
    now := time.Now()  
    then := now.Add(-2 * time.Hour) // 2小时前的时间  

    // 使用Sub方法计算时间差  
    duration := now.Sub(then)  

    // 打印时间差  
    fmt.Printf("时间差: %s\n", duration)  

    // 如果想要获得特定单位的时间差,比如小时数,可以这样做:
    hours := int(duration.Hours())  
    fmt.Printf("时间差的小时数: %d\n", hours)  
}
Equal
func (t Time) Equal(u Time) bool

判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。本方法和用t==u不同,这种方法还会比较地点和时区信息。

package main  

import (  
    "fmt"  
    "time"  
)  

func main() {  
    // 创建两个时间对象  
    now := time.Now()  
    sameNow := now // 假设这是相同的时间,尽管在实际场景中它们可能有些许差异  

    // 使用==操作符比较两个时间是否相等  
    if now.Equal(sameNow) {  
        fmt.Println("两个时间是相等的")  
    } else {  
        fmt.Println("两个时间不相等")  
    }  

    // 注意:直接使用==操作符进行比较即可,因为time.Time已经实现了该操作  
    if now == sameNow {  
        fmt.Println("两个时间确实是相等的")  
    } else {  
        fmt.Println("两个时间不是相等的")  
    }  

    // 创建一个不同的时间对象进行比较  
    differentNow := now.Add(time.Hour)  

    // 再次比较  
    if now == differentNow {  
        fmt.Println("now和differentNow是相等的")  
    } else {  
        fmt.Println("now和differentNow不相等")  
    }  
}
Before
func (t Time) Before(u Time) bool

如果t代表的时间点在u之前,返回真;否则返回假。

func main() {  
    // 创建两个时间对象  
    now := time.Now()  
    past := now.Add(-2 * time.Hour) // 2小时前的时间  

    // 使用Before方法判断past是否在now之前  
    if past.Before(now) {  
        fmt.Println("past时间在now之前")  
    } else {  
        fmt.Println("past时间不在now之前")  
    }  

    // 尝试一个反过来的比较  
    if now.Before(past) {  
        fmt.Println("now时间在past之前")  
    } else {  
        fmt.Println("now时间不在past之前")  
    }  

    // 使用After方法作为对比(它是Before的反向操作)  
    if now.After(past) {  
        fmt.Println("now时间在past之后")  
    } else {  
        fmt.Println("now时间不在past之后")  
    }  
}
After
func (t Time) After(u Time) bool

如果t代表的时间点在u之后,返回真;否则返回假。

2.2.6. 定时器

使用time.Tick(时间间隔)来设置定时器,定时器的本质上是一个通道(channel)。

func tickDemo() {
    ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
    for i := range ticker {
        fmt.Println(i)//每秒都会执行的任务
    }
}

定时器常用场景:

1. 单次定时任务

使用time.Timer可以在一段时间后触发单次任务。这在需要等待一定时间后再执行某些操作的场景中非常有用。

package main  

import (  
    "fmt"  
    "time"  
)  

func main() {  
    // 创建一个定时器,在2秒后触发  
    timer := time.NewTimer(2 * time.Second)  

    // 阻塞等待定时器的信号  
    <-timer.C  

    fmt.Println("定时器触发了!")  

    // 停止定时器,防止资源泄露  
    timer.Stop()  
}
2. 超时控制

time.Timer也可以用于实现超时控制,例如在网络请求或I/O操作中设置超时时间。

package main  

import (  
    "fmt"  
    "net/http"  
    "time"  
)  

func main() {  
    // 创建一个定时器,用于控制请求超时  
    timeout := time.After(5 * time.Second)  

    // 发起HTTP请求  
    resp, err := http.Get("http://example.com")  
    if err != nil {  
        fmt.Println("请求失败:", err)  
        return  
    }  
    defer resp.Body.Close()  

    // 检查是否超时  
    select {  
    case <-timeout:  
        fmt.Println("请求超时!")  
        return  
    default:  
        fmt.Println("请求成功!")  
    }  

    // 处理响应...  
}
3. 周期性任务

使用time.Ticker可以创建周期性触发的任务。这在需要定期执行某项任务(如监控、日志清理、状态更新等)的场景中非常有用。

package main  

import (  
    "fmt"  
    "time"  
)  

func main() {  
    // 创建一个周期为1秒的Ticker  
    ticker := time.NewTicker(1 * time.Second)  
    defer ticker.Stop() // 确保在程序退出前停止Ticker  

    // 循环接收Ticker的通道中的时间  
    for t := range ticker.C {  
        fmt.Println("Ticker触发了:", t)  

        // 在这里执行周期性任务...  
    }  
}
4. 定时检查资源状态

如果你需要定时检查某个资源的状态(如数据库连接、文件状态、服务健康检查等),可以使用定时器来实现。

package main  

import (  
    "fmt"  
    "time"  
)  

func checkResourceStatus() {  
    // 检查资源状态的逻辑...  
    fmt.Println("检查资源状态...")  
}  

func main() {  
    // 创建一个周期为5分钟的Ticker  
    ticker := time.NewTicker(5 * time.Minute)  
    defer ticker.Stop() // 确保在程序退出前停止Ticker  

    for range ticker.C {  
        checkResourceStatus()  
    }  
}

注意事项

  • 当不再需要定时器时,应调用Stop方法来停止它,以避免资源泄露。

  • 定时器触发的精度受限于系统的调度器和计时器的实现,因此不应用于需要高精度计时的场景。

  • 在使用select语句与定时器结合时,要注意select的非阻塞特性,并正确处理各种可能的分支情况。

2.2.7. 时间格式化

时间类型有一个自带的方法Format进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)。也许这就是技术人员的浪漫吧。

补充:如果想格式化为12小时方式,需指定PM。

func formatDemo() {
    now := time.Now()
    // 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan
    // 24小时制
    fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
    // 12小时制
    fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan"))
    fmt.Println(now.Format("2006/01/02 15:04"))
    fmt.Println(now.Format("15:04 2006/01/02"))
    fmt.Println(now.Format("2006/01/02"))
}
解析字符串格式的时间
now := time.Now()
fmt.Println(now)
// 加载时区
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
    fmt.Println(err)
    return
}
// 按照指定时区和指定格式解析字符串时间
timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", "2019/08/04 14:15:20", loc)
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(timeObj)
fmt.Println(timeObj.Sub(now))

2.3 Flag

Go语言内置的flag包实现了命令行参数的解析,flag包使得开发命令行工具更为简单。

2.3.1. os.Args

如果你只是简单的想要获取命令行参数,可以像下面的代码示例一样使用os.Args来获取命令行参数。

package main

import (
    "fmt"
    "os"
)

//os.Args demo
func main() {
    //os.Args是一个[]string
    if len(os.Args) > 0 {
        for index, arg := range os.Args {
            fmt.Printf("args[%d]=%v\n", index, arg)
        }
    }
}

将上面的代码执行go build -o "args_demo"编译之后,执行:

$ ./args_demo a b c d
    args[0]=./args_demo
    args[1]=a
    args[2]=b
    args[3]=c
    args[4]=d

os.Args是一个存储命令行参数的字符串切片,它的第一个元素是执行文件的名称。

或者运行命令

2.3.2. flag包基本使用

本文介绍了flag包的常用函数和基本用法,更详细的内容请查看官方文档[1]

导入flag包
import flag
flag参数类型

flag包支持的命令行参数类型有bool、int、int64、uint、uint64、float float64、string、duration。

flag参数 有效值
字符串flag 合法字符串
整数flag 1234、0664、0x1234等类型,也可以是负数。
浮点数flag 合法浮点数
bool类型flag 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。
时间段flag 任何合法的时间段字符串。如”300ms”、”-1.5h”、”2h45m”。
合法的单位有”ns”、”us” /“µs”、”ms”、”s”、”m”、”h”。
2.3.3. 定义命令行flag参数

有以下两种常用的定义命令行flag参数的方法。

flag.Type()

基本格式如下:

flag.Type(flag名, 默认值, 帮助信息)*Type例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

name := flag.String("name", "张三", "姓名")
age := flag.Int("age", 28, "年龄")
married := flag.Bool("married", false, "婚否")
delay := flag.Duration("d", 0, "时间间隔")

需要注意的是,此时name、age、married、delay均为对应类型的指针。

flag.TypeVar()

基本格式如下:flag.TypeVar(Type指针, flag名, 默认值, 帮助信息) 例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 28, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "时间间隔")
flag.Parse()

通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。

支持的命令行参数格式有以下几种:

  • -flag xxx (使用空格,一个-符号)

  • --flag xxx (使用空格,两个-符号)

  • -flag=xxx (使用等号,一个-符号)

  • --flag=xxx (使用等号,两个-符号)

其中,布尔类型的参数必须使用等号的方式指定。

Flag解析在第一个非flag参数(单个”-“不是flag参数)之前停止,或者在终止符”–“之后停止。

2.3.4. flag其他函数
  • flag.Args() 返回命令行参数后的其他参数,以[]string类型

  • flag.NArg() //返回命令行参数后的其他参数个数

  • flag.NFlag() //返回使用的命令行参数个数

2.3.5. 完整示例
定义
func main() {
    //定义命令行参数方式1
    var name string
    var age int
    var married bool
    var delay time.Duration
    flag.StringVar(&name, "name", "张三", "姓名")
    flag.IntVar(&age, "age", 28, "年龄")
    flag.BoolVar(&married, "married", false, "婚否")
    flag.DurationVar(&delay, "d", 0, "延迟的时间间隔")

    //解析命令行参数
    flag.Parse()
    fmt.Println(name, age, married, delay)
    //返回命令行参数后的其他参数
    fmt.Println(flag.Args())
    //返回命令行参数后的其他参数个数
    fmt.Println(flag.NArg())
    //返回使用的命令行参数个数
    fmt.Println(flag.NFlag())
}
2.3.6. 使用

命令行参数使用提示:

$ ./flag_demo -help
    Usage of ./flag_demo:
      -age int
            年龄 (default 28)
      -d duration
            时间间隔
      -married
            婚否
      -name string
            姓名 (default "张三")

正常使用命令行flag参数:

$ ./flag_demo -name pprof --age 28 -married=false -d=1h30m
    pprof 28 false 1h30m0s
    []
    0
    4

使用非flag命令行参数:

$ ./flag_demo a b c
    张三 28 false 0s
    [a b c]
    3
    0

2.4 log

Go语言内置的log包实现了简单的日志服务。这里介绍了标准库log的基本使用。

2.4.1. 使用Logger

log包定义了Logger类型,该类型提供了一些格式化输出的方法。本包也提供了一个预定义的“标准”logger,可以通过调用函数Print系列(Print|Printf|Println)、Fatal系列(Fatal|Fatalf|Fatalln)、和Panic系列(Panic|Panicf|Panicln)来使用,比自行创建一个logger对象更容易使用。

例如,我们可以像下面的代码一样直接通过log包来调用上面提到的方法,默认它们会将日志信息打印到终端界面:

package main

import (
    "log"
)

func main() {
    log.Println("这是一条很普通的日志。")
    v := "很普通的"
    log.Printf("这是一条%s日志。\n", v)
    log.Fatalln("这是一条会触发fatal的日志。")
    log.Panicln("这是一条会触发panic的日志。")
}

编译并执行上面的代码会得到如下输出:

2019/10/11 14:04:17 这是一条很普通的日志。
    2019/10/11 14:04:17 这是一条很普通的日志。
    2019/10/11 14:04:17 这是一条会触发fatal的日志。

logger会打印每条日志信息的日期、时间,默认输出到系统的标准错误。Fatal系列函数会在写入日志信息后调用os.Exit(1)。Panic系列函数会在写入日志信息后panic。

2.4.2. 配置logger

默认情况下的logger只会提供日志的时间信息,但是很多情况下我们希望得到更多信息,比如记录该日志的文件名和行号等。log标准库中为我们提供了定制这些设置的方法。

log标准库中的Flags函数会返回标准logger的输出配置,而SetFlags函数用来设置标准logger的输出配置。

func Flags() int
func SetFlags(flag int)
2.4.3. flag选项

log标准库提供了如下的flag选项,它们是一系列定义好的常量。

const (
    // 控制输出日志信息的细节,不能控制输出的顺序和格式。
    // 输出的日志在每一项后会有一个冒号分隔:例如2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
    Ldate         = 1 << iota     // 日期:2009/01/23
    Ltime                         // 时间:01:23:23
    Lmicroseconds                 // 微秒级别的时间:01:23:23.123123(用于增强Ltime位)
    Llongfile                     // 文件全路径名+行号:/a/b/c/d.go:23
    Lshortfile                    // 文件名+行号:d.go:23(会覆盖掉Llongfile)
    LUTC                          // 使用UTC时间
    LstdFlags     = Ldate | Ltime // 标准logger的初始值
)

下面我们在记录日志之前先设置一下标准logger的输出选项如下:

func main() {
    log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
    log.Println("这是一条很普通的日志。")
}

编译执行后得到的输出结果如下:

2019/10/11 14:05:17.494943 .../log_demo/main.go:11: 这是一条很普通的日志。
2.4.4. 配置日志前缀

log标准库中还提供了关于日志信息前缀的两个方法:

func Prefix() string
    func SetPrefix(prefix string)

其中Prefix函数用来查看标准logger的输出前缀,SetPrefix函数用来设置输出前缀。

func main() {
    log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
    log.Println("这是一条很普通的日志。")
    log.SetPrefix("[pprof]")
    log.Println("这是一条很普通的日志。")
}

上面的代码输出如下:

[pprof]2019/10/11 14:05:57.940542 .../log_demo/main.go:13: 这是一条很普通的日志。

这样我们就能够在代码中为我们的日志信息添加指定的前缀,方便之后对日志信息进行检索和处理。

2.4.5. 配置日志输出位置
func SetOutput(w io.Writer)

SetOutput函数用来设置标准logger的输出目的地,默认是标准错误输出。

例如,下面的代码会把日志输出到同目录下的xx.log文件中。

func main() {
    logFile, err := os.OpenFile("./xx.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        fmt.Println("open log file failed, err:", err)
        return
    }
    log.SetOutput(logFile)
    log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
    log.Println("这是一条很普通的日志。")
    log.SetPrefix("[小王子]")
    log.Println("这是一条很普通的日志。")
}

如果你要使用标准的logger,我们通常会把上面的配置操作写到init函数中。

go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性。

有下面的特征:

1 init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等

    2 每个包可以拥有多个init函数

    3 包的每个源文件也可以拥有多个init函数

    4 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明)

    5 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序

    6 init函数不能被其他函数调用,而是在main函数执行之前,自动被调用
func init() {
    logFile, err := os.OpenFile("./xx.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        fmt.Println("open log file failed, err:", err)
        return
    }
    log.SetOutput(logFile)
    log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
}
2.4.6. 创建logger

log标准库中还提供了一个创建新logger对象的构造函数–New,支持我们创建自己的logger示例。New函数的签名如下:

func New(out io.Writer, prefix string, flag int) *Logger

New创建一个Logger对象。其中,参数out设置日志信息写入的目的地。参数prefix会添加到生成的每一条日志前面。参数flag定义日志的属性(时间、文件等等)。

举个例子:

func main() {
    logger := log.New(os.Stdout, "<New>", log.Lshortfile|log.Ldate|log.Ltime)
    logger.Println("这是自定义的logger记录的日志。")
}

将上面的代码编译执行之后,得到结果如下:

<New>2019/10/11 14:06:51 main.go:34: 这是自定义的logger记录的日志。

总结 : Go内置的log库功能有限,例如无法满足记录不同级别日志的情况,我们在实际的项目中根据自己的需要选择使用第三方的日志库,如logrus、zap等。

2.5 IO操作

2.5.1 输入输出的底层原理
  • 终端其实是一个文件,相关实例如下:

    • os.Stdin:标准输入的文件实例,类型为*File

    • os.Stdout:标准输出的文件实例,类型为*File

    • os.Stderr:标准错误输出的文件实例,类型为*File

以文件的方式操作终端:

package main

import "os"

func main() {
    var buf [16]byte
    os.Stdin.Read(buf[:])
    os.Stdin.WriteString(string(buf[:]))
}
2.5.2. 文件操作相关API
  • func Create(name string) (file *File, err Error)

    • 根据提供的文件名创建新的文件,返回一个文件对象,默认权限是0666

  • func NewFile(fd uintptr, name string) *File

    • 根据文件描述符创建相应的文件,返回一个文件对象

  • func Open(name string) (file *File, err Error)

    • 只读方式打开一个名称为name的文件

  • func OpenFile(name string, flag int, perm uint32) (file *File, err Error)

    • 打开名称为name的文件,flag是打开的方式,只读、读写等,perm是权限

  • func (file *File) Write(b []byte) (n int, err Error)

    • 写入byte类型的信息到文件

  • func (file *File) WriteAt(b []byte, off int64) (n int, err Error)

    • 在指定位置开始写入byte类型的信息

  • func (file *File) WriteString(s string) (ret int, err Error)

    • 写入string信息到文件

  • func (file *File) Read(b []byte) (n int, err Error)

    • 读取数据到b中

  • func (file *File) ReadAt(b []byte, off int64) (n int, err Error)

    • 从off开始读取数据到b中

  • func Remove(name string) Error

    • 删除文件名为name的文件

2.5.3. 打开和关闭文件

os.Open()函数能够打开一个文件,返回一个*File和一个err。对得到的文件实例调用close()方法能够关闭文件。

package main

import (
    "fmt"
    "os"
)

func main() {
    // 只读方式打开当前目录下的main.go文件
    file, err := os.Open("./main.go")
    if err != nil {
        fmt.Println("open file failed!, err:", err)
        return
    }
    // 关闭文件
    defer file.Close()
}

注意

  • 使用 defer 语句确保文件最终会被关闭,即使发生错误。

2.5.4. 写文件

使用 os.Create 创建文件,然后使用 io.Writer 接口写入内容。

package main  

import (  
    "fmt"  
    "io"  
    "os"  
)  

func main() {  
    file, err := os.Create("output.txt")  
    if err != nil {  
        panic(err)  
    }  
    defer file.Close()  

    writer := io.Writer(file)  
    data := []byte("Hello, World!")  
    _, err = writer.Write(data)  
    if err != nil {  
        panic(err)  
    }  
    fmt.Println("Data written successfully")  
}

注意:

  • 写入时检查返回的错误。

  • Write 方法返回写入的字节数和可能发生的错误。

2.5.5. 读文件

文件读取可以用file.Read()和file.ReadAt(),读到文件末尾会返回io.EOF的错误

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 打开文件
    file, err := os.Open("./xxx.txt")
    if err != nil {
        fmt.Println("open file err :", err)
        return
    }
    defer file.Close()
    // 定义接收文件读取的字节数组
    var buf [128]byte
    var content []byte
    for {
        n, err := file.Read(buf[:])
        if err == io.EOF {//io.EOF是一个预定义的错误值,表示“End Of File”,即文件结束或流结束。
            // 读取结束
            break
        }
        if err != nil {
            fmt.Println("read file err ", err)
            return
        }
        content = append(content, buf[:n]...)
    }
    fmt.Println(string(content))
}

注意:

  • 读取时检查 io.EOF 错误,表示文件读取完毕。

  • 读取到的字节数可能小于缓冲区大小,使用返回的 n 来确定实际读取了多少字节。

2.5.6. 拷贝文件

Go语言拷贝文件一共有三种方法:

1 io.copy()
package main  

import (  
    "fmt"  
    "io"  
    "os"  
)  

func copyFile(src, dst string) error {  
    sourceFile, err := os.Open(src)  
    if err != nil {  
        return err  
    }  
    defer sourceFile.Close()  

    destinationFile, err := os.Create(dst)  
    if err != nil {  
        return err  
    }  
    defer destinationFile.Close()  

    // 使用 io.Copy 来从源文件读取并写入到目标文件  
    _, err = io.Copy(destinationFile, sourceFile)  
    if err != nil {  
        return err  
    }  

    // 确保将文件缓存区的内容刷新到磁盘上  
    return destinationFile.Sync()  
}  

func main() {  
    err := copyFile("source.txt", "destination.txt")  
    if err != nil {  
        panic(err)  
    }  
    fmt.Println("File copied successfully")  
}
2  使用 os.Read() 和 os.Write()
package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 打开源文件
    srcFile, err := os.Open("./xxx.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    // 创建新文件
    dstFile, err2 := os.Create("./abc2.txt")
    if err2 != nil {
        fmt.Println(err2)
        return
    }
    // 缓冲读取
    buf := make([]byte, 1024)
    for {
        // 从源文件读数据
        n, err := srcFile.Read(buf)
        if err == io.EOF {
            fmt.Println("读取完毕")
            break
        }
        if err != nil {
            fmt.Println(err)
            break
        }
        //写出去
        dstFile.Write(buf[:n])
    }
    srcFile.Close()
    dstFile.Close()
}
3  使用 ioutil.WriteFile() 和 ioutil.ReadFile():已过时,不推荐

复制文件的第三种方法是使用 ioutil.ReadFile() 和 ioutil.WriteFile() 函数。第一个函数用于将整个文件的内容,一次性地读入到某个内存中的字节切片里;第二个函数则用于将字节切片的内容写入到一个磁盘文件中。从 Go 1.16 开始,io/ioutil 包的大部分功能已经被标记为过时,并推荐使用新的 os 和 io 包的功能。因此,尽管以下代码仍然有效,但不建议在新的代码中使用它:

package main  

import (  
    "fmt"  
    "io/ioutil"  
    "os"  
)  

func copyFileOld(src, dst string) error {  
    input, err := ioutil.ReadFile(src)  
    if err != nil {  
        return err  
    }  
    err = ioutil.WriteFile(dst, input, 0644)  
    if err != nil {  
        return err  
    }  
    return nil  
}  

func main() {  
    err := copyFileOld("source.txt", "destination.txt")  
    if err != nil {  
        panic(err)  
    }  
    fmt.Println("File copied successfully")  
}
2.5.7. bufio
  • bufio包实现了带缓冲区的读写,是对文件读写的封装

  • bufio缓冲写数据

模式 含义
os.O_WRONLY 只写
os.O_CREATE 创建文件
os.O_RDONLY 只读
os.O_RDWR 读写
os.O_TRUNC 清空
os.O_APPEND 追加
  • bufio读数据

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

func wr() {
    // 参数2:打开模式,所有模式d都在上面
    // 参数3是权限控制
    // w写 r读 x执行   w  2   r  4   x  1
    file, err := os.OpenFile("./xxx.txt", os.O_CREATE|os.O_WRONLY, 0666)
    if err != nil {
        return
    }
    defer file.Close()
    // 获取writer对象
    writer := bufio.NewWriter(file)
    for i := 0; i < 10; i++ {
        writer.WriteString("hello\n")
    }
    // 刷新缓冲区,强制写出
    writer.Flush()
}

func re() {
    file, err := os.Open("./xxx.txt")
    if err != nil {
        return
    }
    defer file.Close()
    reader := bufio.NewReader(file)
    for {
        line, _, err := reader.ReadLine()
        if err == io.EOF {
            break
        }
        if err != nil {
            return
        }
        fmt.Println(string(line))
    }

}

func main() {
    re()
}
2.5.8. ioutil工具包
  • 工具包写文件

  • 工具包读取文件

package main

import (
   "fmt"
   "io/ioutil"
)

func wr() {
   err := ioutil.WriteFile("./yyy.txt", []byte("example.com"), 0666)
   if err != nil {
      fmt.Println(err)
      return
   }
}

func re() {
   content, err := ioutil.ReadFile("./yyy.txt")
   if err != nil {
      fmt.Println(err)
      return
   }
   fmt.Println(string(content))
}

func main() {
   re()
}
2.5.9. 例子
实现一个cat命令

使用文件操作相关知识,模拟实现linux平台cat命令的功能。

package main

import (
    "bufio"
    "flag"
    "fmt"
    "io"
    "os"
)

// cat命令实现
func cat(r *bufio.Reader) {
    for {
        buf, err := r.ReadBytes('\n') //注意是字符
        if err == io.EOF {
            break
        }
        fmt.Fprintf(os.Stdout, "%s", buf)
    }
}

func main() {
    flag.Parse() // 解析命令行参数
    if flag.NArg() == 0 {
        // 如果没有参数默认从标准输入读取内容
        cat(bufio.NewReader(os.Stdin))
    }
    // 依次读取每个指定文件的内容并打印到终端
    for i := 0; i < flag.NArg(); i++ {
        f, err := os.Open(flag.Arg(i))
        if err != nil {
            fmt.Fprintf(os.Stdout, "reading from %s failed, err:%v\n", flag.Arg(i), err)
            continue
        }
        cat(bufio.NewReader(f))
    }
}

2.6 http

Go语言内置的net/http包十分的优秀,提供了HTTP客户端和服务端的实现。

2.6.1. net/http介绍

Go语言内置的net/http包提供了HTTP客户端和服务端的实现。

2.6.2. HTTP协议

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。

2.6.3. HTTP客户端

基本的HTTP/HTTPS请求 Get、Head、Post和PostForm函数发出HTTP/HTTPS请求。

resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...
resp, err := http.PostForm("http://example.com/form",
    url.Values{"key": {"Value"}, "id": {"123"}})

程序在使用完response后必须关闭回复的主体。

resp, err := http.Get("http://example.com/")
if err != nil {
    // handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// ...
2.6.4. GET请求示例

使用net/http包编写一个简单的发送HTTP请求的Client端,代码如下:

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    resp, err := http.Get("https://example.com/")
    if err != nil {
        fmt.Println("get failed, err:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("read from resp.Body failed,err:", err)
        return
    }
    fmt.Print(string(body))
}

将上面的代码保存之后编译成可执行文件,执行之后就能在终端打印网站首页的内容了,我们的浏览器其实就是一个发送和接收HTTP协议数据的客户端,我们平时通过浏览器访问网页其实就是从网站的服务器接收HTTP数据,然后浏览器会按照HTML、CSS等规则将网页渲染展示出来。

2.6.5. 带参数的GET请求示例

关于GET请求的参数需要使用Go语言内置的net/url这个标准库来处理。

func main() {
    apiUrl := "http://example/get"
    // URL param
    data := url.Values{}
    data.Set("name", "阿尘")
    data.Set("age", "28")
    u, err := url.ParseRequestURI(apiUrl)
    if err != nil {
        fmt.Printf("parse url requestUrl failed,err:%v\n", err)
    }
    u.RawQuery = data.Encode() // URL encode
    fmt.Println(u.String())
    resp, err := http.Get(u.String())
    if err != nil {
        fmt.Println("post failed, err:%v\n", err)
        return
    }
    defer resp.Body.Close()
    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("get resp failed,err:%v\n", err)
        return
    }
    fmt.Println(string(b))
}

对应的Server端HandlerFunc如下:

func getHandler(w http.ResponseWriter, r *http.Request) {
    defer r.Body.Close()
    data := r.URL.Query()
    fmt.Println(data.Get("name"))
    fmt.Println(data.Get("age"))
    answer := `{"status": "ok"}`
    w.Write([]byte(answer))
}
2.6.6. Post请求示例

上面演示了使用net/http包发送GET请求的示例,发送POST请求的示例代码如下:

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "strings"
)

// net/http post demo

func main() {
    url := "http://example/post"
    // 表单数据
    //contentType := "application/x-www-form-urlencoded"
    //data := "name=阿尘&age=28"
    // json
    contentType := "application/json"
    data := `{"name":"阿尘","age":28}`
    resp, err := http.Post(url, contentType, strings.NewReader(data))
    if err != nil {
        fmt.Println("post failed, err:%v\n", err)
        return
    }
    defer resp.Body.Close()
    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("get resp failed,err:%v\n", err)
        return
    }
    fmt.Println(string(b))
}

对应的Server端HandlerFunc如下:

func postHandler(w http.ResponseWriter, r *http.Request) {
    defer r.Body.Close()
    // 1. 请求类型是application/x-www-form-urlencoded时解析form数据
    r.ParseForm()
    fmt.Println(r.PostForm) // 打印form数据
    fmt.Println(r.PostForm.Get("name"), r.PostForm.Get("age"))
    // 2. 请求类型是application/json时从r.Body读取数据
    b, err := ioutil.ReadAll(r.Body)
    if err != nil {
        fmt.Println("read request.Body failed, err:%v\n", err)
        return
    }
    fmt.Println(string(b))
    answer := `{"status": "ok"}`
    w.Write([]byte(answer))
}
2.6.7. 自定义Client

如果需要更复杂的请求处理,比如设置自定义的请求头、处理重定向、设置超时、使用TLS客户端证书等,创建一个自定义的Client会更灵活:

client := &http.Client{
    CheckRedirect: redirectPolicyFunc,
}
resp, err := client.Get("http://example.com")
// ...
req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("If-None-Match", `W/"wyzzy"`)
resp, err := client.Do(req)
// ...
package main  

import (  
    "bytes"  
    "fmt"  
    "io/ioutil"  
    "net/http"  
    "time"  
)  

func main() {  
    // 自定义HTTP客户端  
    client := &http.Client{  
        // 设置超时时间  
        Timeout: 5 * time.Second,  
    }  

    // 设置请求的URL  
    url := "http://example.com/post"  

    // 准备请求体数据,这里假设是JSON格式  
    postData := []byte(`{"name":"Alice", "age":30}`)  

    // 创建请求  
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(postData))  
    if err != nil {  
        panic(err)  
    }  

    // 设置请求头,指定Content-Type为application/json  
    req.Header.Set("Content-Type", "application/json")  

    // 发送请求  
    resp, err := client.Do(req)  
    if err != nil {  
        panic(err)  
    }  
    defer resp.Body.Close() // 确保关闭响应体  

    // 读取响应体内容  
    body, err := ioutil.ReadAll(resp.Body)  
    if err != nil {  
        panic(err)  
    }  

    // 输出响应状态码和响应体内容  
    fmt.Println("Response Status:", resp.Status)  
    fmt.Println("Response Body:", string(body))  
}ponse Body:", string(body))  
}

我们可以对请求的各个部分进行详细的定制。同时,通过重用同一个http.Client实例,你还可以利用连接池来提高性能,减少因创建和关闭连接而产生的开销

2.6.8. 自定义Transport

http.Transport结构体包含了控制HTTP客户端行为的各种字段,比如代理设置、TLS配置、连接池大小、超时时间等。,通过自定义http.Transport,你可以根据应用的需求调整各种网络参数,从而优化性能、安全性或满足特定的网络要求,创建一个Transport:

package main  

import (  
    "crypto/tls"  
    "io/ioutil"  
    "net/http"  
    "time"  
)  

func main() {  
    // 自定义Transport  
    transport := &http.Transport{  
        // 设置TLS配置(可选)  
        TLSClientConfig: &tls.Config{  
            InsecureSkipVerify: true, // 注意:这个设置会跳过TLS证书验证,只用于测试或内部网络,不应用于生产环境  
        },  
        // 设置代理(可选)  
        Proxy: http.ProxyFromEnvironment,  
        // 设置连接池的最大空闲连接数(可选)  
        MaxIdleConnsPerHost: 10,  
        // 设置请求超时时间(可选)  
        DialContext: (&net.Dialer{  
            Timeout:   30 * time.Second,  
            KeepAlive: 30 * time.Second,  
        }).DialContext,  
        // 设置TLS握手超时时间(可选)  
        TLSHandshakeTimeout: 10 * time.Second,  
        // 设置响应头读取超时时间(可选)  
        ResponseHeaderTimeout: 10 * time.Second,  
        // 设置期望服务器的响应超时时间(可选)  
        ExpectContinueTimeout: 1 * time.Second,  
    }  

    // 创建自定义的HTTP客户端,并传入自定义的Transport  
    client := &http.Client{  
        Transport: transport,  
    }  

    // 发送请求  
    req, err := http.NewRequest("GET", "http://example.com", nil)  
    if err != nil {  
        panic(err)  
    }  

    resp, err := client.Do(req)  
    if err != nil {  
        panic(err)  
    }  
    defer resp.Body.Close()  

    // 读取响应体  
    body, err := ioutil.ReadAll(resp.Body)  
    if err != nil {  
        panic(err)  
    }  

    // 输出响应内容  
    println(string(body))  
}

注意:

Client和Transport类型都可以安全的被多个go程同时使用。出于效率考虑,应该一次建立、尽量重用。

上述示例中的InsecureSkipVerify: true用于跳过TLS证书验证,这通常只在测试或内部网络中使用,因为它会使通信容易受到中间人攻击。在生产环境中,你应该使用有效的TLS证书,并验证服务器的身份。

2.6.9. 服务端
默认的Server

ListenAndServe使用指定的监听地址和处理器启动一个HTTP服务端。处理器参数通常是nil,这表示采用包变量DefaultServeMux作为处理器。

Handle和HandleFunc函数可以向DefaultServeMux添加处理器。

http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
默认的Server示例

使用Go语言中的net/http包来编写一个简单的接收HTTP请求的Server端示例,net/http包是对net包的进一步封装,专门用来处理HTTP协议的数据。具体的代码如下:

// http server

func sayHello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello 阿尘!")
}

func main() {
    http.HandleFunc("/", sayHello)
    err := http.ListenAndServe(":9090", nil)
    if err != nil {
        fmt.Printf("http server failed, err:%v\n", err)
        return
    }
}

将上面的代码编译之后执行,打开你电脑上的浏览器在地址栏输入example回车,此时就能够看到 Hello 阿尘!

自定义Server

要管理服务端的行为,可以创建一个自定义的Server:

package main  

import (  
    "fmt"  
    "log"  
    "net/http"  
    "time"  
)  

// 定义中间件函数,用于记录请求日志  
func loggingMiddleware(next http.Handler) http.Handler {  
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {  
        start := time.Now()  

        // 调用下一个处理程序  
        next.ServeHTTP(w, r)  

        // 记录请求耗时  
        log.Printf("Request %s %s took %v", r.Method, r.URL, time.Since(start))  
    })  
}  

// 定义路由处理函数  
func helloHandler(w http.ResponseWriter, r *http.Request) {  
    fmt.Fprint(w, "Hello, World!")  
}  

// 定义错误处理函数  
func errorHandler(w http.ResponseWriter, r *http.Request) {  
    http.Error(w, "Internal Server Error", http.StatusInternalServerError)  
}  

func main() {  
    // 创建mux用于路由处理  
    mux := http.NewServeMux()  

    // 添加路由规则,使用中间件包装处理程序  
    mux.Handle("/hello", loggingMiddleware(http.HandlerFunc(helloHandler)))  

    // 添加默认的错误处理路由  
    mux.HandleFunc("/", errorHandler)  

    // 创建并启动服务器  
    server := &http.Server{  
        Addr:         ":8080", // 监听端口  
        Handler:      mux,     // 设置路由处理器  
        ReadTimeout:  5 * time.Second,  
        WriteTimeout: 10 * time.Second,  
    }  

    log.Printf("Starting server on %s", server.Addr)  
    if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {  
        log.Fatalf("Failed to start server: %v", err)  
    }  
}

2.7 context

在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。context包定义了一个Context类型,这个类型可以在多个goroutine之间共享数据,如请求范围的值、取消信号和截止时间。这样,我们可以方便地管理这些goroutine的生命周期,包括设定超时、取消请求等操作

2.7.1. 为什么需要Context

在Go语言中,context的存在主要是为了解决在并发编程中传递请求作用域信息、控制goroutine生命周期以及处理并发操作等问题。具体来说,以下是为什么需要context的几个关键原因:

  1. 状态管理与环境隔离:Context允许程序在执行过程中存储和管理状态信息。当一个函数或goroutine被调用时,它可以将其内部状态保存在Context中,以便在之后的调用或执行中继续使用。此外,Context还可以帮助程序将不同的逻辑隔离开,避免冲突和错误,特别是在多线程或多goroutine编程中。

    package main  
    
    import (  
        "context"  
        "fmt"  
    )  
    
    func main() {  
        // 创建一个带有值的Context  
        ctx := context.WithValue(context.Background(), "key", "value")  
    
        // 传递Context给另一个函数  
        handleRequest(ctx)  
    }  
    
    func handleRequest(ctx context.Context) {  
        // 从Context中获取值  
        if value := ctx.Value("key"); value != nil {  
            fmt.Println("Value from context:", value)  
        }  
    }
  2. 请求作用域信息传递:在Web服务或微服务架构中,一个请求可能会涉及多个组件或服务。在这些组件或服务之间传递请求相关的值(如用户身份、请求ID等)是非常必要的。通过context,这些值可以在请求的处理链中方便地传递,使得各个组件可以共享这些信息,进而提供更准确、一致的服务。

    package main  
    
    import (  
        "context"  
        "fmt"  
        "net/http"  
    )  
    
    func main() {  
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {  
            // 创建带有请求信息的Context  
            ctx := context.WithValue(r.Context(), "requestID", generateRequestID())  
            handleRequest(ctx)  
        })  
    
        http.ListenAndServe(":8080", nil)  
    }  
    
    func generateRequestID() string {  
        // 生成请求ID的逻辑  
        return "request-12345"  
    }  
    
    func handleRequest(ctx context.Context) {  
        // 从Context中获取请求ID  
        if requestID := ctx.Value("requestID"); requestID != nil {  
            fmt.Println("Handling request with ID:", requestID)  
        }  
    }
  3. 控制并发操作:在并发编程中,我们经常需要控制goroutine的生命周期,如设置超时、取消长时间运行的操作等。context提供了这样的机制。通过为context设置截止时间(deadline)或取消信号,我们可以确保当这些条件满足时,所有使用同一个context的goroutine都会接收到通知,并可以相应地停止它们的工作。

    package main  
    
    import (  
        "context"  
        "fmt"  
        "time"  
    )  
    
    func main() {  
        // 创建一个带有截止时间的Context  
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)  
        defer cancel() // 确保最后调用cancel  
    
        // 启动一个goroutine并传递Context  
        go doWork(ctx)  
    
        // 等待goroutine完成或超时  
        select {  
        case <-ctx.Done():  
            if err := ctx.Err(); err != nil {  
                fmt.Println("Work was canceled or timed out:", err)  
            }  
        }  
    }  
    
    func doWork(ctx context.Context) {  
        select {  
        case <-time.After(3 * time.Second):  
            fmt.Println("Work completed")  
        case <-ctx.Done():  
            fmt.Println("Work was canceled:", ctx.Err())  
            return  
        }  
    }
  4. 调试与错误排查:Context还可以用于跟踪程序的执行过程,这对于调试和错误排查非常有帮助。通过记录每个函数的调用栈和执行路径,我们可以轻松地追踪程序的执行流程,更好地理解程序的行为。

    package main  
    
    import (  
        "context"  
        "fmt"  
        "log"  
    )  
    
    func main() {  
        // 创建一个带有日志追踪ID的Context  
        ctx := context.WithValue(context.Background(), "traceID", "trace-123")  
    
        handleRequest(ctx)  
    }  
    
    func handleRequest(ctx context.Context) {  
        // 从Context中获取日志追踪ID  
        if traceID := ctx.Value("traceID"); traceID != nil {  
            log.Printf("Handling request with traceID: %s", traceID)  
        }  
    
        // ... 其余处理逻辑 ...  
    
        // 模拟发生错误  
        if someErrorCondition {  
            log.Printf("Error occurred in request with traceID: %s", traceID)  
            // 处理错误...  
        }  
    }
2.7.2. Context接口

context.Context是一个接口,该接口定义了四个需要实现的方法。具体签名如下:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

其中:

  • Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);

  • Done方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel;

  • Err方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值;

    • 如果当前Context被取消就会返回Canceled错误;

    • 如果当前Context超时就会返回DeadlineExceeded错误;

  • Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;

2.7.3. Background()和TODO()

Go内置两个函数:Background()和TODO(),这两个函数分别返回一个实现了Context接口的background和todo。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。

Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

package main  

import (  
    "context"  
    "fmt"  
    "time"  
)  

func main() {  
    // 创建一个空的Context作为起点  
    ctx := context.Background()  

    // 传递Context到可能执行异步操作的函数  
    doWork(ctx)  

    // 主程序继续执行其他任务或等待异步操作完成  
    fmt.Println("Main program doing other work...")  
    time.Sleep(2 * time.Second) // 模拟其他工作耗时  
}  

func doWork(ctx context.Context) {  
    // 在doWork内部,可以使用ctx来控制goroutine的执行,例如设置超时或取消操作  
    // 这里我们简单打印一条消息表示工作开始  
    fmt.Println("Starting work in doWork")  

    // 假设工作很快完成  
    time.Sleep(1 * time.Second)  
    fmt.Println("Work in doWork completed")  
}

TODO(),它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。

package main  

import (  
    "context"  
    "fmt"  
)  

func main() {  
    // 假设我们正在开发一个新的功能,但还没决定如何传递Context  
    // 在这种情况下,我们可以临时使用context.TODO()  
    processData(context.TODO(), "some data")  
}  

func processData(ctx context.Context, data string) {  
    // 在这里,我们可能会使用ctx来控制操作,但目前只是一个占位符  
    fmt.Println("Processing data:", data)  

    // 假设我们决定在将来为processData提供一个真实的Context来控制操作  
    // 那时,我们就需要替换掉这里的context.TODO()  
}

在上面的 processData 函数中,我们使用了 context.TODO() 来传递一个 Context。这只是一个临时的解决方案,它提醒我们在将来的开发中需要为这个函数提供一个真正的 Context 来控制可能存在的异步操作。

background和todo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

注意context.TODO() 不应该用于生产环境的代码,它仅是一个开发过程中的提醒占位符。在实际开发中,应尽快替换为合适的 Context

2.7.4. With系列函数

此外,context包中还定义了四个With系列函数。

2.7.5. WithCancel

WithCancel的函数签名如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。

取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

func gen(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return // return结束该goroutine,防止泄露
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 当我们取完需要的整数后调用cancel

    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
}

上面的示例代码中,gen函数在单独的goroutine中生成整数并将它们发送到返回的通道。gen的调用者在使用生成的整数之后需要取消上下文,以免gen启动的内部goroutine发生泄漏。

2.7.6. WithDeadline

WithDeadline的函数签名如下:

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

返回父上下文的副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。当截止日过期时,当调用返回的cancel函数时,或者当父上下文的Done通道关闭时,返回上下文的Done通道将被关闭,以最先发生的情况为准。

取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)

    // 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
    // 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }
}

上面的代码中,定义了一个50毫秒之后过期的deadline,然后我们调用context.WithDeadline(context.Background(), d)得到一个上下文(ctx)和一个取消函数(cancel),然后使用一个select让主程序陷入等待:等待1秒后打印overslept退出或者等待ctx过期后退出。因为ctx50秒后就过期,所以ctx.Done()会先接收到值,上面的代码会打印ctx.Err()取消原因。

2.7.7. WithTimeout

WithTimeout的函数签名如下:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout返回WithDeadline(parent, time.Now().Add(timeout))。

取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel,通常用于数据库或者网络连接的超时控制。具体示例如下:

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

// context.WithTimeout

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
    for {
        fmt.Println("db connecting ...")
        time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
        select {
        case <-ctx.Done(): // 50毫秒后自动调用
            break LOOP
        default:
        }
    }
    fmt.Println("worker done!")
    wg.Done()
}

func main() {
    // 设置一个50毫秒的超时
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 5)
    cancel() // 通知子goroutine结束
    wg.Wait()
    fmt.Println("over")
}
2.7.8. WithValue

WithValue函数能够将请求作用域的数据与 Context 对象建立关系。声明如下:

func WithValue(parent Context, key, val interface{}) Context

WithValue返回父节点的副本,其中与key关联的值为val。

仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。

所提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

// context.WithValue

type TraceCode string

var wg sync.WaitGroup

func worker(ctx context.Context) {
    key := TraceCode("TRACE_CODE")
    traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code
    if !ok {
        fmt.Println("invalid trace code")
    }
LOOP:
    for {
        fmt.Printf("worker, trace code:%s\n", traceCode)
        time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
        select {
        case <-ctx.Done(): // 50毫秒后自动调用
            break LOOP
        default:
        }
    }
    fmt.Println("worker done!")
    wg.Done()
}

func main() {
    // 设置一个50毫秒的超时
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    // 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
    ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 5)
    cancel() // 通知子goroutine结束
    wg.Wait()
    fmt.Println("over")
}
2.7.9. 使用Context的注意事项
  1. 避免传递nil
    当给函数方法传递Context时,不要传递nil。如果不知道传递什么,可以使用context.Background()来创建一个空的Context作为起点,或者根据需要使用context.TODO()作为占位符。

  2. 谨慎使用Value
    ContextValue相关方法应该用于传递请求域的必要数据,而不是用于传递可选参数。滥用Value可能导致代码难以理解和维护。尽量避免在Context中存储大量数据或复杂对象,因为它可能会被多个goroutine共享和访问。

  3. 线程安全
    尽管Context是线程安全的,可以在多个goroutine之间安全地传递,但也要注意不要过度依赖Context来管理状态或同步操作。Context的主要目的是取消信号和截止时间的传播,以及请求数据的传递,而不是用于一般的状态管理。

  4. 避免内存泄漏
    使用Context时要特别注意避免内存泄漏。当Context销毁时,如果它仍然被引用,那么销毁可能会失败,导致内存泄漏。确保在不再需要Context时释放其持有的资源,并避免创建长时间存在的Context对象。

  5. 选择合适的Context类型
    根据具体的使用场景选择合适的Context类型。例如,如果需要在goroutine之间传递取消信号,可以使用WithCancel创建的Context;如果需要设置操作的截止时间,可以使用WithDeadlineWithTimeout

  6. 不要滥用WithValue
    虽然WithValue允许在Context中存储键值对数据,但应该谨慎使用。滥用WithValue可能导致Context变得庞大和复杂,难以管理和维护。如果需要在多个层次之间传递数据,可以考虑使用其他机制,如请求作用域的对象或闭包。

  7. 正确处理取消
    当使用WithCancelWithDeadlineWithTimeout创建的Context时,要确保正确处理取消事件。监听ContextDone通道以获取取消信号,并在接收到信号时清理资源、关闭连接或停止goroutine的执行。

2.7.10. 客户端超时取消示例
server端超时设置
// context_timeout/server/main.go
package main

import (
    "fmt"
    "math/rand"
    "net/http"

    "time"
)

// server端,随机出现慢响应

func indexHandler(w http.ResponseWriter, r *http.Request) {
    number := rand.Intn(2)
    if number == 0 {
        time.Sleep(time.Second * 10) // 耗时10秒的慢响应
        fmt.Fprintf(w, "slow response")
        return
    }
    fmt.Fprint(w, "quick response")
}

func main() {
    http.HandleFunc("/", indexHandler)
    err := http.ListenAndServe(":8000", nil)
    if err != nil {
        panic(err)
    }
}
client端超时设置
package main  

import (  
    "context"  
    "fmt"  
    "io/ioutil"  
    "net/http"  
    "time"  
)  

func doCall(ctx context.Context, url string) (string, error) {  
    client := http.Client{  
        // 如果需要全局的client对象,可以定义在包级别,并在这里直接使用  
        // 否则,每次调用doCall时都会创建一个新的client对象  
        // 这里我们直接使用默认的Transport,它会根据情况自动处理keep-alive  
    }  

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)  
    if err != nil {  
        return "", fmt.Errorf("new request failed: %v", err)  
    }  

    resp, err := client.Do(req)  
    if err != nil {  
        // 如果错误是context的取消错误,则返回超时错误  
        if err == context.Canceled {  
            return "", fmt.Errorf("call api timeout")  
        }  
        return "", fmt.Errorf("call server api failed: %v", err)  
    }  
    defer resp.Body.Close()  

    data, err := ioutil.ReadAll(resp.Body)  
    if err != nil {  
        return "", fmt.Errorf("read response failed: %v", err)  
    }  

    return string(data), nil  
}  

func main() {  
    url := "http://127.0.0.1:8000/"  
    // 定义一个100毫秒的超时  
    timeout := time.Millisecond * 100  
    ctx, cancel := context.WithTimeout(context.Background(), timeout)  
    defer cancel() // 调用cancel释放资源  

    resp, err := doCall(ctx, url)  
    if err != nil {  
        fmt.Println(err)  
        return  
    }  
    fmt.Printf("resp: %s\n", resp)  
})  
    fmt.Printf("响应内容: %s\n", string(body))  
}

2.8 数据格式

Go关于数据标准库常用的有JSON、xml、csv、gob

2.8.1 encoding/json
  • json使用go语言内置的encoding/json 标准库

  • 编码json使用json.Marshal()函数可以对一组数据进行JSON格式的编码

func Marshal(v interface{}) ([]byte, error)

使用结构体创建json

package main  

import (  
    "encoding/json"  
    "fmt"  
    "log"  
)  

// 定义一个结构体来表示数据  
type Person struct {  
    Name    string `json:"name"`  
    Age     int    `json:"age"`  
    Address struct {  
        City   string `json:"city"`  
        Street string `json:"street"`  
    } `json:"address"`  
}  

func main() {  
    // 创建一个Person实例  
    p := Person{  
        Name: "John Doe",  
        Age:  30,  
        Address: struct {  
            City   string `json:"city"`  
            Street string `json:"street"`  
        }{  
            City:   "New York",  
            Street: "123 Main St",  
        },  
    }  

    // 将Person实例序列化为JSON  
    jsonData, err := json.Marshal(p)  
    if err != nil {  
        log.Fatalf("Failed to marshal person to JSON: %v", err)  
    }  

    // 将序列化后的JSON数据转换为字符串并打印  
    jsonString := string(jsonData)  
    fmt.Println(jsonString)  
}

使用map创建JSON,

package main  

import (  
    "encoding/json"  
    "fmt"  
    "log"  
)  

func main() {  
    // 创建一个map,键为字符串类型,值为任意类型(这里使用interface{})  
    data := make(map[string]interface{})  
    data["name"] = "John Doe"  
    data["age"] = 30  
    data["isStudent"] = false  
    data["grades"] = map[string]float64{  
        "math":    90.5,  
        "science": 85.0,  
    }  

    // 将map转换为JSON字节切片  
    jsonData, err := json.Marshal(data)  
    if err != nil {  
        log.Fatalf("Failed to marshal map to JSON: %v", err)  
    }  

    // 将JSON字节切片转换为字符串并打印  
    jsonString := string(jsonData)  
    fmt.Println(jsonString)  
}

确定json结构,解析到结构体

package main  

import (  
    "encoding/json"  
    "fmt"  
    "log"  
)  

// 定义一个与JSON数据对应的结构体  
type Person struct {  
    Name    string `json:"name"`  
    Age     int    `json:"age"`  
    Email   string `json:"email"`  
    Address struct {  
        City   string `json:"city"`  
        Street string `json:"street"`  
    } `json:"address"`  
    PhoneNumbers []string `json:"phoneNumbers"`  
}  

func main() {  
    // JSON字符串  
    jsonString := `  
    {  
        "name": "John Doe",  
        "age": 30,  
        "email": "john@example.com",  
        "address": {  
            "city": "New York",  
            "street": "123 Main St"  
        },  
        "phoneNumbers": ["123-456-7890", "234-567-8901"]  
    }  
    `  

    // 创建一个Person实例  
    var person Person  

    // 解析JSON字符串到结构体中  
    err := json.Unmarshal([]byte(jsonString), &person)  
    if err != nil {  
        log.Fatalf("Failed to unmarshal JSON: %v", err)  
    }  

    // 打印解析后的结构体数据  
    fmt.Printf("Name: %s\n", person.Name)  
    fmt.Printf("Age: %d\n", person.Age)  
    fmt.Printf("Email: %s\n", person.Email)  
    fmt.Printf("Address City: %s\n", person.Address.City)  
    fmt.Printf("Address Street: %s\n", person.Address.Street)  
    fmt.Printf("Phone Numbers: %v\n", person.PhoneNumbers)  
}

位置json具体结果,解析到interface

package main  

import (  
    "encoding/json"  
    "fmt"  
    "log"  
)  

func main() {  
    // JSON字符串  
    jsonString := `  
    {  
        "name": "John Doe",  
        "age": 30,  
        "isStudent": false  
    }  
    `  

    // 定义一个interface{}变量来存储解析后的JSON数据  
    var result interface{}  

    // 解析JSON字符串到interface{}中  
    err := json.Unmarshal([]byte(jsonString), &result)  
    if err != nil {  
        log.Fatalf("Failed to unmarshal JSON: %v", err)  
    }  

    // 打印解析后的interface{}数据(通常会是map[string]interface{}类型)  
    fmt.Printf("%+v\n", result)  

    // 如果需要,你可以进行类型断言来访问具体字段的值  
    if data, ok := result.(map[string]interface{}); ok {  
        if name, ok := data["name"].(string); ok {  
            fmt.Println("Name:", name)  
        }  
        if age, ok := data["age"].(float64); ok {  
            fmt.Println("Age:", int(age))  
        }  
        if isStudent, ok := data["isStudent"].(bool); ok {  
            fmt.Println("Is Student:", isStudent)  
        }  
    }  
}
2.8.2 encoding/xml

encoding/xml 是Go语言的一个标准库,用于处理XML数据的编码和解码。通过它,你可以方便地将Go语言中的数据结构序列化为XML格式的数据,或者将XML格式的数据反序列化为Go语言中的数据结构。

使用 encoding/xml 包,你通常需要做的是:

  1. 定义数据结构:首先,你需要定义与XML数据对应的Go语言结构体。结构体中的字段名可以通过标签(tag)来指定与XML元素之间的映射关系。

  2. 编码:使用 encoding/xml 提供的 Marshal 函数,你可以将Go语言中的结构体编码为XML字节流。

  3. 解码:同样,使用 encoding/xml 提供的 Unmarshal 函数,你可以将XML字节流解码为Go语言中的结构体。

示例

假设你有以下的XML数据:

<person>  
    <name>John Doe</name>  
    <age>30</age>  
</person>

你可以定义如下的Go语言结构体来与之对应:

package main  

import (  
    "encoding/xml"  
    "fmt"  
    "log"  
)  

type Person struct {  
    XMLName xml.Name `xml:"person"`  
    Name    string   `xml:"name"`  
    Age     int      `xml:"age"`  
}  

func main() {  
    // XML 数据  
    xmlData := []byte(`<person><name>John Doe</name><age>30</age></person>`)  

    // 定义Person结构体实例  
    var p Person  

    // 解码XML数据到结构体  
    err := xml.Unmarshal(xmlData, &p)  
    if err != nil {  
        log.Fatalf("error unmarshaling XML: %v", err)  
    }  

    // 输出解码后的数据  
    fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age)  

    // 编码结构体为XML数据  
    xmlBytes, err := xml.Marshal(p)  
    if err != nil {  
        log.Fatalf("error marshaling XML: %v", err)  
    }  

    // 输出编码后的XML数据  
    fmt.Println(string(xmlBytes))  
}

在这个例子中,Person 结构体通过字段标签来指定与XML元素之间的映射关系。xml.Unmarshal 函数用于将XML数据解码到结构体中,而 xml.Marshal 函数用于将结构体编码为XML数据。

注意事项

  • 字段标签中的XML元素名称必须与XML数据中的元素名称匹配。

  • 如果XML元素是可选的,而Go语言结构体中的字段是必需的,你可能需要使用指针类型或omitempty选项来正确处理空值。

  • 对于嵌套的XML元素,你可以在Go语言结构体中使用嵌套的结构体来表示。

  • encoding/xml 还提供了其他的功能,比如处理字符数据(CDATA)、命名空间等,你可以根据具体需求查阅相关文档来了解更多细节。

2.8.3 encoding/csv

在Go语言中,处理CSV(逗号分隔值)文件的标准库是encoding/csv。这个库提供了一组函数和类型,使得读取和写入CSV文件变得相对简单。

  1. 读取CSV文件:使用csv.NewReader创建一个新的CSV读取器,然后使用ReadReadAll方法读取数据。

    package main  
    
    import (  
        "encoding/csv"  
        "fmt"  
        "os"  
    )  
    
    func main() {  
        // 打开CSV文件  
        file, err := os.Open("example.csv")  
        if err != nil {  
            panic(err)  
        }  
        defer file.Close()  
    
        // 创建CSV读取器  
        reader := csv.NewReader(file)  
    
        // 逐行读取CSV数据  
        records, err := reader.ReadAll()  
        if err != nil {  
            panic(err)  
        }  
    
        // 输出读取到的数据  
        for _, record := range records {  
            fmt.Println(record) // record是一个[]string,表示CSV的一行  
        }  
    }
  2. 写入CSV文件:使用csv.NewWriter创建一个新的CSV写入器,然后使用WriteWriteAll方法写入数据。

    package main  
    
    import (  
        "encoding/csv"  
        "os"  
    )  
    
    func main() {  
        // 创建CSV文件  
        file, err := os.Create("output.csv")  
        if err != nil {  
            panic(err)  
        }  
        defer file.Close()  
    
        // 创建CSV写入器  
        writer := csv.NewWriter(file)  
        defer writer.Flush() // 确保所有数据都写入文件  
    
        // 定义要写入的数据  
        records := [][]string{  
            {"Name", "Age"},  
            {"Alice", "30"},  
            {"Bob", "25"},  
        }  
    
        // 写入数据  
        for _, record := range records {  
            if err := writer.Write(record); err != nil {  
                panic(err)  
            }  
        }  
    }
2.8.4 encoding/gob

Gob是Go语言特有的二进制序列化格式。它允许你将Go语言中的数据结构编码为二进制格式,然后再解码回原始的数据结构。Gob编码通常比JSON或XML更紧凑,但它是Go语言特有的,因此与其他语言的互操作性较差

package main  

import (  
    "bytes"  
    "encoding/gob"  
    "fmt"  
)  

// 自定义类型,需要在使用gob之前进行注册  
type Person struct {  
    Name string  
    Age  int  
}  

func main() {  
    // 注册自定义类型  
    gob.Register(Person{})  

    // 创建一个Person实例  
    person := Person{Name: "Alice", Age: 30}  

    // 编码  
    var buf bytes.Buffer  
    enc := gob.NewEncoder(&buf)  
    if err := enc.Encode(person); err != nil {  
        panic(err)  
    }  

    // 解码  
    var decodedPerson Person  
    dec := gob.NewDecoder(&buf)  
    if err := dec.Decode(&decodedPerson); err != nil {  
        panic(err)  
    }  

    // 打印解码后的Person实例  
    fmt.Printf("Decoded Person: %+v\n", decodedPerson)  
}

注意事项

  1. 类型注册:在使用gob之前,必须注册所有自定义类型。未注册的类型在编码或解码时会导致错误。

  2. 稳定性:由于gob编码是基于Go的类型系统的,因此如果数据结构发生更改(例如,添加或删除字段),则可能导致无法正确解码旧数据。因此,在版本控制方面需要特别小心。

  3. 安全性gob编码不应该用于不信任的数据源,因为它不是为安全性设计的。它可能被恶意构造的数据利用来执行未授权的操作或导致程序崩溃。

  4. 循环引用:虽然gob支持循环引用(例如,结构体包含指向自身的指针),但应谨慎使用,因为不正确的处理可能导致无限循环或内存泄漏。

  5. 兼容性gob编码是Go特有的,并且与其他语言的标准库不兼容。如果你需要与非Go语言交互,可能需要使用JSON、XML或其他跨语言的编码格式。

2.8.5 MSGPack
  • MSGPack 并不是Go语言的标准库之一,但它是一个高效的二进制序列化格式库,可以在Go语言中使用。MSGPack 的设计目标类似于 JSON,但提供了更小的序列化大小和更快的序列化和反序列化速度。

  • MSGPack 不是一个标准库,但由于其高效性和跨语言兼容性,它在一些特定的应用场景中非常受欢迎

  • 需要安装第三方包:go get -u github.com/vmihailenco/msgpack

    注意:Go 1.16版本开始需要在项目目录里面安装,便于管理和维护项目的依赖关系,不再支持在模块外面使用go get命令

package main  

import (  
    "fmt"  
    "github.com/vmihailenco/msgpack"  
    "os"  
    "math/rand"  
    "time"   
    ) 
type Person struct {
   Name string
   Age  int
   Sex  string
}

// 二进制写出  
func writeMsgpack(filename string) error {  
    var persons []*Person  
    // 设置随机种子  
    rand.Seed(time.Now().UnixNano())  
    // 假数据  
    for i := 0; i < 10; i++ {  
        p := &Person{  
            Name: fmt.Sprintf("name%d", i),  
            Age:  rand.Intn(100),  
            Sex:  "male",  
        }  
        persons = append(persons, p)  
    }  
    // 二进制序列化  
    data, err := msgpack.Marshal(persons)  
    if err != nil {  
        return err  
    }  
    // 写入文件  
    return os.WriteFile(filename, data, 0666)  
}  

// 二进制读取  
func readMsgpack(filename string) error {  
    var persons []*Person  
    // 读文件  
    data, err := os.ReadFile(filename)  
    if err != nil {  
        return err  
    }  
    // 反序列化  
    if err := msgpack.Unmarshal(data, &persons); err != nil {  
        return err  
    }  
    for _, v := range persons {  
        fmt.Printf("%#v\n", v)  
    }  
    return nil  
}  

func main() {  
    // 写入数据到文件  
    if err := writeMsgpack("D:/person.dat"); err != nil {  
        fmt.Println(err)  
        return  
    }  
    // 从文件读取数据  
    if err := readMsgpack("D:/person.dat"); err != nil {  
        fmt.Println(err)  
        return  
    }  
}

2.9 反射

反射是指在程序运行期对程序本身进行访问和修改的能力

Go语言的反射标准库提供了在运行时检查和操作对象的能力,它位于reflect包中。反射在Go语言中有着广泛的应用,特别是在处理泛型编程、动态类型检查、序列化/反序列化、ORM(对象关系映射)等方面。

2.9.1. 反射的使用
  • reflect包封装了反射相关的方法

  • 获取类型信息:reflect.TypeOf,是静态的

  • 获取值信息:reflect.ValueOf,是动态的

2.9.2. 空接口与反射
  • 反射可以在运行时动态获取程序的各种详细信息

  • 反射获取interface类型信息

package main

import (
   "fmt"
   "reflect"
)

//反射获取interface类型信息

func reflect_type(a interface{}) {
   t := reflect.TypeOf(a)
   fmt.Println("类型是:", t)
   // kind()可以获取具体类型
   k := t.Kind()
   fmt.Println(k)
   switch k {
   case reflect.Float64:
      fmt.Printf("a is float64\n")
   case reflect.String:
      fmt.Println("string")
   }
}

func main() {
   var x float64 = 3.4
   reflect_type(x)
}
  • 反射获取interface值信息

package main

import (
    "fmt"
    "reflect"
)

//反射获取interface值信息

func reflect_value(a interface{}) {
    v := reflect.ValueOf(a)
    fmt.Println(v)
    k := v.Kind()
    fmt.Println(k)
    switch k {
    case reflect.Float64:
        fmt.Println("a是:", v.Float())
    }
}

func main() {
    var x float64 = 3.4
    reflect_value(x)
}
  • 反射修改值信息

package main

import (
    "fmt"
    "reflect"
)

//反射修改值
func reflect_set_value(a interface{}) {
    v := reflect.ValueOf(a)
    k := v.Kind()
    switch k {
    case reflect.Float64:
        // 反射修改值
        v.SetFloat(6.9)
        fmt.Println("a is ", v.Float())
    case reflect.Ptr:
        // Elem()获取地址指向的值
        v.Elem().SetFloat(7.9)
        fmt.Println("case:", v.Elem().Float())
        // 地址
        fmt.Println(v.Pointer())
    }
}

func main() {
    var x float64 = 3.4
    // 反射认为下面是指针类型,不是float类型
    reflect_set_value(&x)
    fmt.Println("main:", x)
}
2.9.3. 结构体与反射

查看类型、字段和方法

package main

import (
    "fmt"
    "reflect"
)

// 定义结构体
type User struct {
    Id   int
    Name string
    Age  int
}

// 绑方法
func (u User) Hello() {
    fmt.Println("Hello")
}

// 传入interface{}
func Poni(o interface{}) {
    t := reflect.TypeOf(o)
    fmt.Println("类型:", t)
    fmt.Println("字符串类型:", t.Name())
    // 获取值
    v := reflect.ValueOf(o)
    fmt.Println(v)
    // 可以获取所有属性
    // 获取结构体字段个数:t.NumField()
    for i := 0; i < t.NumField(); i++ {
        // 取每个字段
        f := t.Field(i)
        fmt.Printf("%s : %v", f.Name, f.Type)
        // 获取字段的值信息
        // Interface():获取字段对应的值
        val := v.Field(i).Interface()
        fmt.Println("val :", val)
    }
    fmt.Println("=================方法====================")
    for i := 0; i < t.NumMethod(); i++ {
        m := t.Method(i)
        fmt.Println(m.Name)
        fmt.Println(m.Type)
    }

}

func main() {
    u := User{1, "zs", 20}
    Poni(u)
}

查看匿名字段

package main

import (
    "fmt"
    "reflect"
)

// 定义结构体
type User struct {
    Id   int
    Name string
    Age  int
}

// 匿名字段
type Boy struct {
    User
    Addr string
}

func main() {
    m := Boy{User{1, "zs", 20}, "bj"}
    t := reflect.TypeOf(m)
    fmt.Println(t)
    // Anonymous:匿名
    fmt.Printf("%#v\n", t.Field(0))
    // 值信息
    fmt.Printf("%#v\n", reflect.ValueOf(m).Field(0))
}

修改结构体的值

package main

import (
    "fmt"
    "reflect"
)

// 定义结构体
type User struct {
    Id   int
    Name string
    Age  int
}

// 修改结构体值
func SetValue(o interface{}) {
    v := reflect.ValueOf(o)
    // 获取指针指向的元素
    v = v.Elem()
    // 取字段
    f := v.FieldByName("Name")
    if f.Kind() == reflect.String {
        f.SetString("kuteng")
    }
}

func main() {
    u := User{1, "example.com", 20}
    SetValue(&u)
    fmt.Println(u)
}

调用方法

package main

import (
    "fmt"
    "reflect"
)

// 定义结构体
type User struct {
    Id   int
    Name string
    Age  int
}

func (u User) Hello(name string) {
    fmt.Println("Hello:", name)
}

func main() {
    u := User{1, "example.com", 20}
    v := reflect.ValueOf(u)
    // 获取方法
    m := v.MethodByName("Hello")
    // 构建一些参数
    args := []reflect.Value{reflect.ValueOf("6666")}
    // 没参数的情况下:var args2 []reflect.Value
    // 调用方法,需要传入方法的参数
    m.Call(args)
}

获取字段的tag

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name string `json:"name1" db:"name2"`
}

func main() {
    var s Student
    v := reflect.ValueOf(&s)
    // 类型
    t := v.Type()
    // 获取字段
    f := t.Elem().Field(0)
    fmt.Println(f.Tag.Get("json"))
    fmt.Println(f.Tag.Get("db"))
}

注意事项

  1. 性能:反射操作通常比直接操作要慢,因为它涉及到在运行时进行类型检查和转换。因此,在性能敏感的代码部分,应尽量避免使用反射。

  2. 可设置性:不是所有的reflect.Value都是可设置的(settable)。例如,通过reflect.ValueOf(x)获取的reflect.Value是不可设置的,因为它是对x的副本的引用。为了修改x的值,你需要通过reflect.ValueOf(&x).Elem()获取其地址的值的引用。

  3. 类型安全:虽然反射可以提供很大的灵活性,但它也破坏了Go语言的静态类型安全。在使用反射时,应格外小心,确保类型操作的正确性。

  4. 接口:反射经常与接口一起使用,因为接口可以持有任意类型的值。通过反射,你可以检查接口中值的实际类型,并进行相应的操作。

2.10 template

1.1.1. 模板示例

通过将模板应用于一个数据结构(即该数据结构作为模板的参数)来执行,来获得输出。模板中的注释引用数据接口的元素(一般如结构体的字段或者字典的键)来控制执行过程和获取需要呈现的值。模板执行时会遍历结构并将指针表示为’.‘(称之为”dot”)指向运行过程中数据结构的当前位置的值。

用作模板的输入文本必须是utf-8编码的文本。”Action”—数据运算和控制单位—由”“界定;在Action之外的所有文本都不做修改的拷贝到输出中。Action内部不能有换行,但注释可以有换行。

HTML文件代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
    <p>Hello {{.}}</p>
</body>
</html>

我们的HTTP server端代码如下:

// main.go

func sayHello(w http.ResponseWriter, r *http.Request) {
    // 解析指定文件生成模板对象
    tmpl, err := template.ParseFiles("./hello.html")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    // 利用给定数据渲染模板,并将结果写入w
    tmpl.Execute(w, "example.com")
}
func main() {
    http.HandleFunc("/", sayHello)
    err := http.ListenAndServe(":9090", nil)
    if err != nil {
        fmt.Println("HTTP server failed,err:", err)
        return
    }
}
1.1.2. 模板语法
{{.}}

模板语法都包含在{{和}}中间,其中{{.}}中的点表示当前对象。

当我们传入一个结构体对象时,我们可以根据.来访问结构体的对应字段。例如:

// main.go

type UserInfo struct {
    Name   string
    Gender string
    Age    int
}

func sayHello(w http.ResponseWriter, r *http.Request) {
    // 解析指定文件生成模板对象
    tmpl, err := template.ParseFiles("./hello.html")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    // 利用给定数据渲染模板,并将结果写入w
    user := UserInfo{
        Name:   "阿尘",
        Gender: "男",
        Age:    28,
    }
    tmpl.Execute(w, user)
}

HTML文件代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
    <p>Hello {{.Name}}</p>
    <p>性别:{{.Gender}}</p>
    <p>年龄:{{.Name}}</p>
</body>
</html>

同理,当我们传入的变量是map时,也可以在模板文件中通过.根据key来取值。

注释
{{/* a comment */}}
    注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止。
pipeline

pipeline是指产生数据的操作。比如{{.}}{{.Name}}等。Go的模板语法中支持使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。

注意 : 并不是只有使用了|才是pipeline。Go的模板语法中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline。

变量

Action里可以初始化一个变量来捕获管道的执行结果。初始化语法如下:

$variable := pipeline

其中$variable是变量的名字。声明变量的action不会产生任何输出。

1.1.3. 条件判断

Go模板语法中的条件判断有以下几种:

{{if pipeline}} T1 {{end}}

{{if pipeline}} T1 {{else}} T0 {{end}}

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
range

Go的模板语法中使用range关键字进行遍历,有以下两种写法,其中pipeline的值必须是数组、切片、字典或者通道。

{{range pipeline}} T1 {{end}}
如果pipeline的值其长度为0,不会有任何输出

{{range pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值其长度为0,则会执行T0。
with
{{with pipeline}} T1 {{end}}
如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。

{{with pipeline}} T1 {{else}} T0 {{end}}
如果pipeline为empty,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。
1.1.4. 预定义函数

执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里。

预定义的全局函数如下:

and
    函数返回它的第一个empty参数或者最后一个参数;
    就是说"and x y"等价于"if x then y else x";所有参数都会执行;
or
    返回第一个非empty参数或者最后一个参数;
    亦即"or x y"等价于"if x then x else y";所有参数都会执行;
not
    返回它的单个参数的布尔值的否定
len
    返回它的参数的整数类型长度
index
    执行结果为第一个参数以剩下的参数为索引/键指向的值;
    如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
print
    即fmt.Sprint
printf
    即fmt.Sprintf
println
    即fmt.Sprintln
html
    返回其参数文本表示的HTML逸码等价表示。
urlquery
    返回其参数文本表示的可嵌入URL查询的逸码等价表示。
js
    返回其参数文本表示的JavaScript逸码等价表示。
call
    执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
    如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
    其中Y是函数类型的字段或者字典的值,或者其他类似情况;
    call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
    该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型;
    如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;

示例代码:

package main  

import (  
    "os"  
    "text/template"  
)  

func main() {  
    tmpl := `{{len .}} items in total.`  
    nums := []int{1, 2, 3, 4, 5}  

    t, err := template.New("nums").Parse(tmpl)  
    if err != nil {  
        panic(err)  
    }  

    err = t.Execute(os.Stdout, nums)  
    if err != nil {  
        panic(err)  
    }  
}
1.1.5. 比较函数

布尔函数会将任何类型的零值视为假,其余视为真。

下面是定义为函数的二元比较运算的集合:

eq      如果arg1 == arg2则返回真
    ne      如果arg1 != arg2则返回真
    lt      如果arg1 < arg2则返回真
    le      如果arg1 <= arg2则返回真
    gt      如果arg1 > arg2则返回真
    ge      如果arg1 >= arg2则返回真

为了简化多参数相等检测,eq(只有eq)可以接受2个或更多个参数,它会将第一个参数和其余参数依次比较,返回下式的结果:

{{eq arg1 arg2 arg3}}

比较函数只适用于基本类型(或重定义的基本类型,如”type Celsius float32”)。但是,整数和浮点数不能互相比较。

1.1.6. 自定义函数

Go的模板支持自定义函数。

func sayHello(w http.ResponseWriter, r *http.Request) {
    htmlByte, err := ioutil.ReadFile("./hello.html")
    if err != nil {
        fmt.Println("read html failed, err:", err)
        return
    }
    // 自定义一个夸人的模板函数
    kua := func(arg string) (string, error) {
        return arg + "真帅", nil
    }
    // 采用链式操作在Parse之前调用Funcs添加自定义的kua函数
    tmpl, err := template.New("hello").Funcs(template.FuncMap{"kua": kua}).Parse(string(htmlByte))
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }

    user := UserInfo{
        Name:   "阿尘",
        Gender: "男",
        Age:    28,
    }
    // 使用user渲染模板,并将结果写入w
    tmpl.Execute(w, user)
}

我们可以在模板文件hello.html中使用我们自定义的kutgkua函数了。

{{kua .Name}}
1.1.7. 嵌套template

我们可以在template中嵌套其他的template。这个template可以是单独的文件,也可以是通过define定义的template。

举个例子:t.html文件内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>tmpl test</title>
</head>
<body>

    <h1>测试嵌套template语法</h1>
    <hr>
    {{template "ul.html"}}
    <hr>
    {{template "ol.html"}}
</body>
</html>

{{ define "ol.html"}}
<h1>这是ol.html</h1>
<ol>
    <li>吃饭</li>
    <li>睡觉</li>
    <li>打豆豆</li>
</ol>
{{end}}

ul.html文件内容如下:

<ul>
    <li>注释</li>
    <li>日志</li>
    <li>测试</li>
</ul>

我们注册一个templDemo路由处理函数.

http.HandleFunc("/tmpl", tmplDemo)

tmplDemo函数的具体内容如下:

func tmplDemo(w http.ResponseWriter, r *http.Request) {
    tmpl, err := template.ParseFiles("./t.html", "./ul.html")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    user := UserInfo{
        Name:   "阿尘",
        Gender: "男",
        Age:    28,
    }
    tmpl.Execute(w, user)
}

2.11  Strconv

strconv 是 Go 语言标准库中的一个包,它提供了一系列函数来执行字符串和基本数据类型之间的转换。下面是一些 strconv 包中常用的方法和操作,以及相应的示例代码:

2.11.1. Atoi (字符串到整数)

将字符串转换为整数。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	s := "123"  
	i, err := strconv.Atoi(s)  
	if err != nil {  
		fmt.Println("转换错误:", err)  
	} else {  
		fmt.Println("转换结果:", i)  
	}  
}
2.11.2. ParseInt (字符串到整数)

将字符串转换为指定基数的整数。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	s := "1010"  
	base := 2 // 二进制  
	i, err := strconv.ParseInt(s, base, 64) // 64表示结果至少为int64类型  
	if err != nil {  
		fmt.Println("转换错误:", err)  
	} else {  
		fmt.Println("转换结果:", i)  
	}  
}
2.11.3. ParseUint (字符串到无符号整数)

将字符串转换为指定基数的无符号整数。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	s := "12345"  
	base := 10 // 十进制  
	ui, err := strconv.ParseUint(s, base, 64) // 64表示结果至少为uint64类型  
	if err != nil {  
		fmt.Println("转换错误:", err)  
	} else {  
		fmt.Println("转换结果:", ui)  
	}  
}
2.11.4. ParseFloat (字符串到浮点数)

将字符串转换为浮点数。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	s := "3.14159"  
	f, err := strconv.ParseFloat(s, 64) // 64表示结果至少为float64类型  
	if err != nil {  
		fmt.Println("转换错误:", err)  
	} else {  
		fmt.Println("转换结果:", f)  
	}  
}
2.11.5. Itoa (整数到字符串)

将整数转换为字符串。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	i := 123  
	s := strconv.Itoa(i)  
	fmt.Println("转换结果:", s)  
}
2.11.6. FormatInt (整数到格式化字符串)

将整数格式化为字符串。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	i := int64(12345)  
	s := strconv.FormatInt(i, 10) // 10表示十进制  
	fmt.Println("转换结果:", s)  
}
2.11.7. FormatUint (无符号整数到格式化字符串)

将无符号整数格式化为字符串。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	ui := uint64(12345)  
	s := strconv.FormatUint(ui, 10) // 10表示十进制  
	fmt.Println("转换结果:", s)  
}
2.11.8. FormatFloat (浮点数到格式化字符串)

将浮点数格式化为字符串。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	f := 3.14159  
	s := strconv.FormatFloat(f, 'f', 2, 64) // 'f'表示定点表示法,2表示小数点后保留两位,64表示f为float64类型  
	fmt.Println("转换结果:", s)  
}
2.11.9. Unquote (去引号)

strconv 包中的 Unquote 函数用于移除字符串中的引号(双引号或单引号),并解析其中的转义序列。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	s := `"Hello, World!"`  
	unquoted, err := strconv.Unquote(s)  
	if err != nil {  
		fmt.Println("解析错误:", err)  
	} else {  
		fmt.Println("去引号后的结果:", unquoted)  
	}  
}

在这个例子中,字符串 s 包含一个双引号包围的字符串 "Hello, World!"strconv.Unquote 函数会移除这些引号,并解析其中的任何转义字符(如果有的话),然后返回去引号后的字符串。

2.11.10. Quote (加引号)

Quote 函数用于给字符串添加双引号,并转义其中的特殊字符。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	s := `Hello, "World"!`  
	quoted := strconv.Quote(s)  
	fmt.Println("加引号后的结果:", quoted)  
}

在这个例子中,字符串 s 包含了一个双引号字符。strconv.Quote 函数会给整个字符串加上双引号,并转义其中的双引号字符,返回一个加引号后的字符串。

2.11.11. AppendBool (追加布尔值到字节切片)

AppendBool 函数将布尔值转换为字符串,并将其追加到字节切片中。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	buf := make([]byte, 0, 10)  
	buf = strconv.AppendBool(buf, true)  
	fmt.Println("追加后的字节切片:", buf)  
	fmt.Println("转换为字符串:", string(buf))  
}
2.11.12. AppendInt, AppendUint, AppendFloat (追加整数、无符号整数、浮点数到字节切片)

这些函数类似于 AppendBool,但它们分别用于追加整数、无符号整数和浮点数到字节切片。

package main  
  
import (  
	"fmt"  
	"strconv"  
)  
  
func main() {  
	buf := make([]byte, 0, 10)  
	buf = strconv.AppendInt(buf, 123, 10)  
	buf = strconv.AppendUint(buf, 456, 10)  
	buf = strconv.AppendFloat(buf, 3.14159, 'f', 2, 64)  
	fmt.Println("追加后的字节切片:", buf)  
	fmt.Println("转换为字符串:", string(buf))  
}

这些是 strconv 包中一些常用的函数和方法。它们为字符串和基本数据类型之间的转换提供了方便且高效的方式。在实际编程中,根据需要选择合适的函数来处理字符串和数字之间的转换。

3 常用命令

go env用于打印Go语言的环境信息。

go run命令可以编译并运行命令源码文件。

go get可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。

go build命令用于编译我们指定的源码文件或代码包以及它们的依赖包。

go install用于编译并安装指定的代码包及它们的依赖包。

go clean命令会删除掉执行其它命令时产生的一些文件和目录。

go doc命令可以打印附于Go语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的。

go test命令用于对Go语言编写的程序进行测试。

go list命令的作用是列出指定的代码包的信息。

go fix会把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码。

go vet是一个用于检查Go语言源码中静态错误的简单工具。

go tool pprof命令来交互式的访问概要文件的内容。

$ go
Go is a tool for managing Go source code.

Usage:

    go command [arguments]

The commands are:

    build       compile packages and dependencies
    clean       remove object files
    doc         show documentation for package or symbol
    env         print Go environment information
    bug         start a bug report
    fix         run go tool fix on packages
    fmt         run gofmt on package sources
    generate    generate Go files by processing source
    get         download and install packages and dependencies
    install     compile and install packages and dependencies
    list        list packages
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         run go tool vet on packages

Use "go help [command]" for more information about a command.

Additional help topics:

    c           calling between Go and C
    buildmode   description of build modes
    filetype    file types
    gopath      GOPATH environment variable
    environment environment variables
    importpath  import path syntax
    packages    description of package lists
    testflag    description of testing flags
    testfunc    description of testing functions

Use "go help [topic]" for more information about that topic.

参考文档: Go语言中文文档

参考资料

[1]

官方文档: https://studygolang.com/pkgdoc

扫描二维码关注阿尘blog,一起交流学习

eb5f3c59949fd7f97a607d26f0b2ccfe.png

相关推荐

  1. Python函数

    2024-03-16 18:32:01       20 阅读
  2. GO语言工具函数--Lancet

    2024-03-16 18:32:01       35 阅读
  3. 【matlab】MATLAB函数&示例

    2024-03-16 18:32:01       38 阅读
  4. Python 函数,及实例演示

    2024-03-16 18:32:01       18 阅读
  5. go语言-字符串处理函数

    2024-03-16 18:32:01       26 阅读
  6. [知识点]C 标准字符串处理函数

    2024-03-16 18:32:01       9 阅读
  7. C++标准函数(长期更新中)

    2024-03-16 18:32:01       9 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-16 18:32:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-16 18:32:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-16 18:32:01       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-16 18:32:01       20 阅读

热门阅读

  1. 【TypeScript系列】声明合并

    2024-03-16 18:32:01       26 阅读
  2. python前端开发

    2024-03-16 18:32:01       26 阅读
  3. 数据智能——企业从业务数据化走向业务智能化

    2024-03-16 18:32:01       23 阅读
  4. 游戏测试 - 二面 - 创梦天地二面

    2024-03-16 18:32:01       24 阅读
  5. 测试覆盖率那些事

    2024-03-16 18:32:01       22 阅读
  6. C/C++蓝桥杯之整数拼接(较难)

    2024-03-16 18:32:01       21 阅读