Go语言之常用库

encoding/json:

encoding/json是官方提供的标准json, 实现RFC 7159中定义的JSON编码和解码。使用的时候需要预定义struct,原理是通过reflectioninterface来完成工作, 性能低。

常用的接口:

  • func Marshal(v interface{}) ([]byte, error)  //生成JSON,参数为空接口,表示接受任意类型参数,返回字节切片
  • func Unmarshal(data []byte, v interface{}) error  //解析JSON到struct,参数为字节切片以及任意类型
  • func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) //生成json,返回字节切片,格式好看

1、JSON序列化

(1)、下面例子表示通过Marshal()将结构体序列化为son数据,如图:

运行结果如下:

  • 上图中第25行:os.Stdout.Write()用法与fmt.Println()相同,fmt.Println()是在os.Stdout.Write()基础上进行了封装,添加了格式化输出,上图中如果用fmt来打印,应该写成fmt.Println(string(result))
  • 第10-12行:结构体的字段名首字母需要大写,否则无法被json解析,Marshal进行序列化的时候是区分大小写的,因此结构体和实例化结构体时字段名必须一致,都是大写

注意:Marshal()返回类型为字节切片,如果要正常显示,需要转换为字符串,因此可写成 fmt.Println(string(result))

(2)、下面例子将结构体切片序列化为JSON数据,如图:

运行结果如下:

(3)、将map序列化为JSON数据,如图:

运行结果如下:

(4)、将结构体指针切片序列化为字符串切片,如图:

package main

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

type abc struct {
	Name string
	Age  string
}

var a []*abc

func main() {
	//定义结构体指针切片
	a = []*abc{
		{Name: "hello", Age: "100"},
		{Name: "world", Age: "200"},
	}
	//将结构体指针切片序列化为字节切片
	by, err := json.Marshal(a)
	if err != nil {
		log.Fatal(err)
	}
	var b []map[string]string
	//将字节切片反序列化为map切片
	errs := json.Unmarshal(by, &b)
	if errs != nil {
		log.Fatal(errs)
	}
	var d []string
	//遍历map切片,然后将里面的map元素反序列化为字节切片后添加到字符串切片中
	for _, v := range b {
		c, err := json.Marshal(v)
		if err != nil {
			log.Fatal(err)
		}
		d = append(d, string(c))
	}
	fmt.Println(d)
}

运行结果如下:

2、JSON反序列化

(1)、下面例子表示通过 Unmarshal()将json数据解析到结构体中(结构体字段名要大写),如图:

  • 第10-13行:将json数据转换为[]byte字节切片类型,并赋值给变量jsonToStruct,发引号表示里面的内容不能转义,但是可以换行,如果不用反引号而用双引号,那么json数据的双引号都要转义,否则报错
  • 第14行:声明结构体,字段名必须大写
  • 第19行:声明结构体类型的切片
  • 第20行:将字节切片解析(反序列化)到结构体person中,Unmarshal的第一个参数为字节切片类型,第二个参数必须为指针类型,否则无法接收数据,Unmarshal反序列化不区分大小写,因此第11-12行的字段名改为小写也是可以的

(2)、将数据单独写在外面,不放在[]byte()中,如图:

(3)、下面是一个将[]byte切片反序列化到结构体指针切片的例子,如图:

执行结果如下:

(4)、下面例子是将[]byte反序列化到结构体中,返回为结构体指针,如图:

运行结果如下:

(5)、将JSON数据反序列化到map中,如图:

程序运行结果为:

(6)、将一个二维切片反序列化到结构体切片中,如图:

运行结果如下:

说明:上面二维切片中用的反引号也可以起到将json数据作为字符串使用,用反引号的好处是json里面的双引号都不用转义了,如果不用反引号而用双引号,那么json里的双引号都要进行转义,如果要讲字节切片反序列化到结构体切片中,那么转换为字节切片的字符串中必须包含[ ]符号,因此s1就是拼接[ ]

3、下面例子表示使用MarshalIndent()和Marshal()生成json,并对比结果,如图:

运行结果可以看出,通过MarshalIndent()生成的json更加直观好看,如图:

从名字上看出,MarshalIndent()和Marshal()之间相差一个Indent()函数,Indent()对读的结果做了一些处理,简单说就是对Json 多了一些格式处理,MarshalIndent()函数实现里就调用了Indent()

注意:结构体的字段的首字母必须大写,否则解析不到

flag

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

1、使用的时候需要导入flag包,可执行命令import flag即可

2、flag包支持的命令行参数类型有boolintint64uintuint64floatfloat64stringduration(时间区间)

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

3、有以下两种常用的定义命令flag参数的方法:

第一种,flag.Type(),语法格式如下:

flag.Type(flag名, 默认值, 帮助信息)

例如:定义姓名、年龄、性别、日期等命令行参数,可使用如下方式:

name := flag.String("name", "张三", "姓名")
age := flag.Int("age", 30, "年龄")
gender := flag.Bool("gender", false, "男")
datetime := flag.Duration("datetime", 0, "时间间隔")

需要注意的是,此时nameage、 gender 、datetime均为对应类型的指针

第二种,flag.TypeVar(),语法格式如下:

flag.TypeVar(Type指针, flag名, 默认值, 帮助信息)

例如:定义姓名、年龄、性别、日期等命令行参数,可使用如下方式:

var name string
var age int
var gender bool
var datetime time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 38, "年龄")
flag.BoolVar(&gender, "gender", false, "男")
flag.DurationVar(&datetime, "datetime", 0, "时间间隔")

4、flag.Parse()

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

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

  • -flag xxx (使用空格,一个"-“符号)
  • --flag xxx (使用空格,两个”-"符号)
  • -flag=xxx (使用等号,一个"-“符号)
  • --flag=xxx (使用等号,两个"-“符号)

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

下面例子表示通过flag.TypeVar()方式定义命令行参数,并解析,如图:

正确使用命令行参数如下:

flag其他函数:

flag.NArg()  //返回命令行参数后的其他参数个数
flag.Args()  //返回命令行参数后的其他参数,以[]string类型
flag.NFlag() //返回使用的命令行参数个数

bufio

1、通过bufio.NewReader实现从标准输入中读取数据(可以是键盘输入),如图:

  • 4-8行:需要导入的相关包
  • 13行:从标准输入中读取数据,可以理解为键盘输入
  • 14行:从标准输入读数据后,直到按了Enter后,也就是换行符后,停止,返回字符串
  • 15行-17行:判断错误是否为nil,log.Fatal表示如果有错误立即停止程序
  • 18行:strings.TrimSpace()表示去除字符串两边的空格,中间的空格保留

代码运行结果如下:

2、通过bufio.Read([]byte)将Reader读取到字节切片中,语法如下:

func (b *Reader) Read(p []byte) (n int, err error)

下面例子读取本地的file.txt文件,并将数据写入到字节切片中,最后打印,如图:

注意:上面的b.Read(c)返回的第一个元素是实际写入切片c中的字节数,可通过此字节数来对切片b进行裁剪获取一个新的切片(不裁剪那么c切片的长度可能比实际的字节数要多,如果反序列化到结构体会出问题)

3、通过bufio和net/http实现文件下载

strconv

1、通过strconv.Atoi()函数,将字符串类型的数字转换为int,如图:

  • 11行:定义字符串,为数字类型的字符串
  • 12行:打印变量类型
  • 13行:将字符串转换为整数
  • 14-16行:判断转换过程中是否有错误
  • 18行:打印转换后的变量类型

代码执行结果如下:

2、通过strconv.Itoa()函数将int类型转换为字符串类型,如图:

3、通过bufio.NewScanner读取文件,strconv.ParseFloat转换类型为float64,如图:

  • 13行:打开文件data.txt
  • 14-16行:如果打开文件出错,停止并退出程序
  • 17行:为文件创建一个Scanner,通过go doc bufio NewScanner可查看 用法
  • 18行:通过Scan()方法从文件中读物一行文本,读取成功返回true,失败返回false
  • 19行:通过ParseFloat将读取内容转换为float64,通过go doc strconv ParseFloat可查看用法

strings

1、通过函数strings.HasPrefix函数判断字符串是否以指定的字母开头,如图:

运行结果如下:

2、 通过函数strings.HasSuffix函数判断字符串是否以指定的字母结尾,如图:

3、通过函数strings.Contains()来判断字符串中是否包含指定的字符,如图:

4、通过Index()判断子字符串或者字符在父字符串中出现的位置,即索引,如图:

5、通过LastIndex()判断字符在父字符串中最后一次出现的索引,如图:

6、通过Replace()将指定的字符替换为指定的字符,如图:

注:Replace最后的-1表示替换所有

7、统计指定字符串或字符出现的非重叠次数,如图:

8、通过Repeat()函数来重复字符串,如图:

9、通过函数ToLower()函数,将字符串中的所有字符都转换为小写,如图:

10、通过函数ToUpper()函数,将字符串中的所有字符转换为大写,如图:

11、通过TrimSpace()函数,去除字符串开头和结尾的空格,如图:

12、通过Trim()和TrimSpace()组合来去除字符串开头和结尾的指定字符,如图:

math/rand

math/rand包有一个Intn函数,可以生成一个随机数,所以我们需要导入math/rand。然后调用rand.Intn生成随机数

1、下面例子表示生成一个0到100之间的随机数(不包括100)以及任意一个随机数,如图:

打印结果为:

注:rand.Intn(int n)函数生成从0-n的随机数(不包括n),rand.Int()函数生成随机数,如果要生成1到100之间随机数,可通过rand.Intn(100) + 1 来实现,实际匹配为0-101,但是不包括101,上图中的时间种子必须要设置,否则每次生成的随机数都是固定的一个

  • UnixNano():从时间点January 1, 1970 UTC到当前时间点所经过的时间(单位纳秒)
  • Unix():从时间点January 1, 1970 UTC到当前时间点所经过的时间(单位秒)

OS

os包为golang内置的包,可实现多种文件操作

1、下面例子通过os.Args读取命令行参数,计算和以及平均数,如图:

  • 11行:通过os.Args实现接收命令行参数,接收后为一个切片,可通过go doc os Args查看用法,如果不从[1:] 开始,那么默认会将文件名放在第一个元素位置
  • 12行:定义变量,类型为float64,初始值为0
  • 13-19:循环遍历切片,将元素类型转换为float64,并求和
  • 20行:计算传入数字的和,并保留2位小数
  • 21行:获取切片的长度,并将长度转换为float64类型
  • 22行:执行平均数运算,只有相同类型才可以进行运算,因此上一步需要转换长度类型

运行脚本,传递四个参数,运行结果如下:

下面例子也是打印os.Args参数信息,如图:

运行结果如下:

从上图结果看出,切片中一共四个元素,第一个元素就是可执行文件所在全路径,剩下三个元素为执行文件时候指定的三个参数

2、下面通过os.Stat()函数获取文件或文件夹的信息,如图:

3、下面通过os.IsNotExist()函数判断文件或文件夹是否存在,如图:

注:本例子中文件path.go实际是存在的

4、通过os.IsExist()函数判断文件或者文件夹是否存在,如图:

本例子中paths.go文件实际是不存在

golang判断文件或文件夹是否存在的方法为使用os.Stat()函数返回的错误值进行判断:

  • 如果返回的错误为nil,说明文件或文件夹存在
  • 如果返回的错误类型使用os.IsNotExist() 或者 os.IsExist() 判断为true,说明文件或文件夹不存在
  • 如果返回的错误为其它类型,则不确定是否在存在

5、通过os.IsPermission()函数判断文件或文件夹是否有权限访问,如图:

文件file.go是存在的,并且root有权限访问,如图:

注:如果文件不存在那么默认为没有权限访问,将返回false

6、通过os.Mkdir和os.MkdirAll创建目录,如图:

  • 如果目录存在会报错,因此在创建之前需要判断一下
  • os.ModePerm:官方写的目录权限为0777,实际测试权限为0755

注: Mkdir()只能创建一个目录,MkdirAll()可以创建多级目录

7、通过os.Rename()重命名文件夹,如图:

path/filepath

1、通过filepath.Join()函数将路径和文件名进行拼接,如图:

代码执行结果如下:

2、通过filepath.Base()来获取文件的最后一个元素,要么是文件夹名字要么是文件名,如图:

运行结果如下:

注:filepath.Base()函数用于获取指定路径的最后一个元素(文件名或目录名),并返回该元素的名称

exec

exec库可用来执行一些命令,通过go doc exec 可查看其用法

1、通过CombinedOutput()函数获取标准输出与标准错误输出,如图:

  • 第12行:调用exec包的Command函数来构造执行对象,返回结果为Cmd结构体的指针(通过go doc exec Command可查看),赋值给cmd,此cmd可自定义,与Cmd不是一个,bash 和-c可不写,直接命令就行,如果遇到管道符(|) ,就需要写bash 和 -c了
  • 第13行:调用结构体Cmd的CombinedOutput方法执行命令并获取标准输出与标准错误输出,注意返回的类型为字节类型,[]byte
  • 第14行-16:判断是否有错误,如果有停止程序并打印错误
  • 第17行:如果没有错误,将字节类型转换为字符串后打印输出

注:如果命令有多个参数,那么每个参数都可以单独作为一个字符串,比如执行ansible命令,如下:

package main

import (
        "fmt"
        "log"
        "os/exec"
)

func main() {
        host := "192.168.49.224"
        src := "/tmp/b.jar"
        dest := "/data/linshi"
        //执行命令
        //cmd := exec.Command("ansible",fmt.Sprintf("%s",host), "-m", "command" ,"-a", "touch /tmp/exec.txt")
        //copy文件
        cmd := exec.Command("ansible", fmt.Sprintf("%s", host), "-m", "copy", "-a", fmt.Sprintf("src=%s dest=%s", src, dest))
        out, err := cmd.CombinedOutput()
        if err != nil {
                log.Fatal(err)
        }
        fmt.Println(string(out))
}

2、通过Output()函数获取标准输出,唯独变化的就是第13行,其余没动,如图:

注意:exec.Command只是构造要执行的命令,实际是cmd.Output()来执行命令并获取标准输出

3、通过buffer将stdout和stderr分别处理,如图:

  • 第15行:声明字节缓冲变量stdout和stderr
  • 第16-17行:获取stdout和stderr的指针地址赋值给cmd.Stdout和cmd.Stderr,由于是传递的指针,因此当cmd.Stdout和cmd.Stderr变化后,原地址stdout和stderr的内容也会变化
  • 18行:通过Run()方法执行命令(go doc exec Run查看用法),返回错误
  • 22行:调用Bytes()方法(go doc bytes Bytes查看用法),返回字节切片,通过string转换为字符串

4、在Windows的命令行执行exec,命令中包含管道符,可通过命令传递的方式

如果要在windows的cmd窗口获取机器的UUID,直接通过命令获取为

WMIC csproduct get UUID | findstr 2

如果要使用golang的exec命令来执行,方法如下:

  • 第12行:StdoutPipe()将会创建一个管道,将命令的标准输出链接到该管道
  • 第16行:将cmd1标准输出作为cmd2的标准输入,从而实现了管道传递
  • 第18行:获取cmd2的标准输出

5、执行多个命令,通过二维切片方式,如图:

  • Command:至少需要两个参数,因此把公共的参数ansible放在第一位,其余的从切片获取
  • v… :通过此方式可将一维切片中的字符串全部拆出来作为参数

Output()和Run()方法的区别:

  • Output方法返回命令的标准输出和错误,并且阻塞直到命令完成,并返回命令的输出,如果需要获取命令的输出,应该使用Output方法
  • Run方法只返回错误,并且阻塞直到命令完成,但不返回命令的输出,如果只关心命令是否成功执行,而不需要获取命令的输出,可以使用Run方法

path

path库为golang自带的库,可以实现获取目录、文件拓展名、获取文件名等,通过go doc path查看用法

1、获取文件拓展名,如图:

注:返回值是路径最后一个斜杠分隔出的路径元素的最后一个’.‘起始的后缀(包括’.’)。如果该元素没有’.’会返回空字符串

2、获取文件名,如图:

注:Base函数返回路径的最后一个元素;在提取元素前会去掉末尾的斜杠。如果路径是””,会返回”.”;如果路径只有一个斜杠,会返回”/”

3、获取文件所在路径,如图:

Dir返回路径去除最后一个路径元素的部分,即该路径最后一个元素所在的目录,使用Split去掉最后一个元素后,会简化路径并去掉末尾的斜杠。如果路径是空字符串,会返回”.”;如果路径由1到多个斜杠后跟0到多个非斜杠字符组成,会返回”/”;其他任何情况下都不会返回以斜杠结尾的路径

4、将文件和路径进行拼接,如图:

运行结果如下:

更多用法可自行网上查找

ssh

ssh库可用于连接远程的linux机器,库全名为:

golang.org/x/crypto/ssh

1、下面是一段完成的通过windows机器连接Linux并执行ls -l /root命令的代码,如下:

package main

import (
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"strings"

	"golang.org/x/crypto/ssh"
)

func main() {
	host := "ip:22"
	sshConfig := &ssh.ClientConfig{
		User: "root",
		Auth: []ssh.AuthMethod{
			ssh.Password("password"),
		},

		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {

                        //读取本地保存的known_hosts文件
			rekey, err := ioutil.ReadFile("known_hosts")
			if err != nil {
				log.Fatal(err)
			}

                        //解析读取的内容
			_, hostnames, pubKey, _, _, err := ssh.ParseKnownHosts(rekey)
			if err != nil {
				log.Fatal(err)
			}

                        //进行比较,相等返回nil,否则返回错误
			if strings.Split(hostname, ":")[0] == hostnames[0] && bytes.Equal(pubKey.Marshal(), key.Marshal()) {
				return nil
			}
			return errors.New("主机公钥验证失败")
		},
	}

        //通过tcp进行连接
	client, err := ssh.Dial("tcp", host, sshConfig)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

        //创建会话
	session, err := client.NewSession()
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

         //执行命令
	res, err := session.CombinedOutput("ls -l /root")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(res))
}

说明:

  • host:定义要远程连接的IP和端口
  • sshConfig:配置要连接的主机的信息,User表示用户名,Auth里配置密码
  • HostKeyCallback:连接成功后执行的一段回调函数,验证主机密钥,通过ReadFile读取known_host的内容,然后对内容进行解析,解析后获取内容中的主机名和公钥内容,将此内容与回调函数从服务器获取的主机名和公钥进行对比,如果相同那么返回nil,继续连接,否则返回错误,断开连接,本例子中的known_hosts为我手动从用户的.ssh目录复制过来到代码目录下的

know_hosts文件是哪里来的?

当通过ssh第一次连接远程主机并成功后,会在本地的.ssh路径下的know_hosts下记录远程主机的主机名+公钥信息,windows路径在用户目录下(linux也是如此),如图:

每次通过ssh连接的时候,客户端都会根据本地存储的这个公钥信息验证远程机器的公钥信息,如果相同就可以连接,不同无法连接,这样可以保证连接的安全性,防止被攻击,如果远程主机的秘钥发生变动,那么本地的known_host也需要及时更改,否则无法连接

注意:上面例子中如果想不通过主机公钥验证(不安全),此时可将返回值设置为nil,即可,如下:

HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
           return nil
}

net/http

1、通过net/http包来发送POST请求到gin的接口,并上传2个文件,如下:

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
)

func main() {
	filepaths := []string{"a.go", "b.go"}
	rb := &bytes.Buffer{}
	//创建一个multipart格式的写入器
	wr := multipart.NewWriter(rb)
	for _, files := range filepaths {
		file, err := os.Open(files)
		if err != nil {
			log.Fatal(err)
		}
		defer file.Close()
		//调用CreatePart来创建新的multipart部分,向multipart格式的写入器中添加多个部分
		//filepath.Base()用来返回路径的最后一个元素,要么是文件要么是路径
		part, err := wr.CreatePart(map[string][]string{
			"Content-Disposition": {"form-data; name=\"file\"; filename=\"" + filepath.Base(files) + "\""},
		})
		if err != nil {
			log.Fatal(err)
		}
		_, err = io.Copy(part, file)
		if err != nil {
			log.Fatal(err)
		}

	}
	//调用Close()方法来结束写入器并生成multipart格式的内容
	wr.Close()
	url := "http://127.0.0.1:8080/api/v1/docToPdf"
	contentType := wr.FormDataContentType()
	resp, err := http.Post(url, contentType, rb)
	if err != nil {
		log.Fatal(err)
	}
	//响应体需要关闭
	defer resp.Body.Close()
	by := make([]byte, 1024) //定义字节切片并初始化,不初始化后面读入不进去
	rd := bufio.NewReader(resp.Body) //读取响应体内容
	bytes, errs := rd.Read(by)   //将内容读入到切片by中,需要初始化分配空间,否则读入不进去
	if errs != nil {
		log.Fatal(errs)
	}
	fmt.Println(string(by),bytes) //bytes为实际读入到切片中的字节数量
}

注意:上面的name=”file”,这个file可以自定义值,但是要与服务端的保持一致,也就是说服务端的gin中获取文件的名称也要是file才行,如果这里改了,那里也要同步修改

2、通过net/http来上传文件的另一种写法,如下:

  1 package main
  2 
  3 import (
  4         "bytes"
  5         "fmt"
  6         "io"
  7         "io/ioutil"
  8         "log"
  9         "mime/multipart"
 10         "net/http"
 11         "os"
 12         "path/filepath"
 13 )
 14 
 15 func main() {
 16         client := &http.Client{} //创建一个http的client实例,用于发送http请求
 17         bf := &bytes.Buffer{}  //创建Buffer实例,用于在内存中存储发送的nultipart数据
 18         writer := multipart.NewWriter(bf) //创建writer实例,写入到buffer中,writer后续用于构建multipart/form-data请求体
 19         req, err := http.NewRequest("POST", "http://127.0.0.1:8080/upload", nil)
 20         if err != nil {
 21                 log.Fatal(err)
 22         }
 23         //设置请求头,writer.FormDataContentType方法返回一个Content-Type,格式为multipart/form-data;boundary=<boundary>,<boundary> 是一个随机生成的唯一字符串,用于分隔请求体中的不同部分
 24         req.Header.Set("Content-Type", writer.FormDataContentType())
 25         fp := "/root/b.jar"
 26         file, err := os.Open(fp)
 27         if err != nil {
 28                 log.Fatal(err)
 29         }
 30         defer file.Close()
 31         //创建表单文件字段,字段名为filename,filepath.Base可以截取路径最后的文件名
 32         part, err := writer.CreateFormFile("filename", filepath.Base(fp))
 33         if err != nil {
 34                 log.Fatal(err)
 35         }
 36         //将文件数据流复制到表单字段part中
 37         _, err = io.Copy(part, file)
 38         if err != nil {
 39                 log.Fatal(err)
 40         }
 41         err = writer.Close()
 42         if err != nil {
 43                 log.Fatal(err)
 44         }
 45         //writer.WriteField("fieldName","fieldValue")  //添加其他字段
 46         req.Body = io.NopCloser(bf)
 47         resp, err := client.Do(req)
 48         if err != nil {
 49                 log.Fatal("cuowu:",err)
 50         }
 51         defer resp.Body.Close()
 52         bodys, err := ioutil.ReadAll(resp.Body)
 53         if err != nil {
 54                 log.Fatal(err)
 55         }
 56         fmt.Println("Body:", string(bodys))
 57 }
  • 46行:io.NopCloser将bf包装为ReadCloser类型的数据,因为Do()要读取req.Body,而Body的类型就是io.ReadCloser,因此需要将bf包装为此类型才可以,因此io.NopCloser的主要作用就是将 io.Reader转换为 io.ReadCloser
  • 47行:Do()方法在发送数据的时候,会读取request中的Body数据,也就是上面的req.Body,也会读取method、url等信息

什么时候需要设置请求头?

简单的 GET 请求(不带认证、不带特殊格式要求)是不需要设置请求头的,如下:

req, _ := http.NewRequest("GET", "https://httpbin.org/get", nil)

需要认证或者传递数据的时候就需要设置请求头,如下:

发送 JSONContent-Type: application/json
发送表单Content-Type: application/x-www-form-urlencoded
上传文件(multipart)Content-Type: multipart/form-data; boundary=…
需要认证Authorization: Bearer <token>
自定义 UAUser-Agent: my-app/1.0
接受特定格式Accept: application/json

设置请求头方式:

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer YOUR_TOKEN")
或者
req.Header.Add("Accept", "application/json")
req.Header.Add("X-Custom-Header", "value")

Set和Add是有区别的,如果Set中出现多个同名的键,只保留最后一个,后面的覆盖前面的,Add不会,支持追加,可能产生多个同名的 Header 字段,如下:

req.Header.Set("X-Token", "abc")
req.Header.Set("X-Token", "xyz")
// 最终 Header 中只有:X-Token: xyz
req.Header.Add("X-Token", "abc")
req.Header.Add("X-Token", "xyz")
// 最终 Header 中有两个值:
// X-Token: abc
// X-Token: xyz

3、通过net/http包来发送GET请求到gin的接口,如下:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
)

func main() {
	//访问此url会下载pdf.zip文件
	url := "http://127.0.0.1:8100/api/v1/GetPdfFile"
	resp, err := http.Get(url)
	if err != nil {
		log.Fatal(err)
	}
	if resp.StatusCode == 200 {
		fmt.Println("文件下载成功")
	}
	defer resp.Body.Close()
	//创建一个pdf.zip文件
	pdf, err := os.Create("/tmp/pdf.zip")
	if err != nil {
		log.Fatal(err)
	}
	defer pdf.Close()
	//将Get获取的响应体数据copy到pdf.zip中
	_, err = io.Copy(pdf, resp.Body)
	if err != nil {
		log.Fatal(err)
	}
}

sort

1、sort.Strings函数对切片中元素按照字母进行排序,如图:

运行结果如下:

builtin

builtin 包为Go的预声明标识符提供了文档,此包中包含了我们常用的一些标识符,比如int8、int16、int32、int64等等,除此之外,在新版本golang 1.21版本中还新增了几个函数

1、min()函数,用于返回最小值,语法如下:

func min[T cmp.Ordered](x T, y ...T) T

min函数为泛型函数,在传递参数的时候根据参数类型来设置变量T的类型,下面获取数字的最小值,如图:

min函数不仅可以获取整数最小值,也可以获取字符串,浮点数等等

2、max()函数,用于返回最大值,语法如下:

func max[T cmp.Ordered](x T, y ...T) T

max函数也是泛型函数,下面例子来获取最大值,如图:

3、clear()函数,语法如下:

func clear[T ~[]Type | ~map[Type]Type1](t T)

clear函数的参数也是泛型函数,入参为mapslice,如果是 map,则删除 map 中的所有元素,返回一个空 map;如果是 slice,则将 slice 中的所有元素改为切片类型的零值

(1)、下面定义一个map,然后调用clear()函数,如图:

运行结果如下:

从上图看出,clear中传入map后,会将map的内容清空变为空的map

(2)、下面定义一个slice,然后传入clear()中,如图:

运行结果如下:

从上图可以看出,map调用clear后被清空,slice调用clear后元素变为零值

io/ioutil

1、通过ioutil.ReadDir()读取目录,返回一个fs.FileInfo切片和error,如图:

2、可根据返回切片的长度来判断目录是否为空,本例子中guan目录下没文件,如图:

reflect

在Golang中,数组、切片、映射等不能直接使用==进行比较,此时可通过reflect包来比较

1、通过reflect.DeepEqual函数比较两个二维切片,如图:

注:切片是否相等要看元素的内容和顺序来决定的,如果把b中的注释位置放在上面,与a中的顺序不同,他们就不相等了

md5和hex

1、计算文件的md5的值,如图:

2、通过md5库和bufio来计算文件的md5的值,如图:

3、通过md5库和hex库来计算字符串(不是文件)的md5的值,如图:

4、只通过md5库来计算字符串的md5值,如图:

  • Sum() 返回的值为[Size]byte,[Size]byte 表示一个固定长度的字节数组,其中 Size 是数组的长度。这种类型通常用于表示具有固定大小的数据结构,例如哈希值,当你看到一个函数返回类型为 [Size]byte,这意味着该函数返回一个长度固定的字节数组。在哈希函数的上下文中,Size 通常是指哈希算法产生的哈希值的字节长度。例如,MD5哈希算法产生的哈希值长度为16字节,SHA-1为20字节,SHA-256为32字节,等等

net/smtp实现邮件发送

通过net/smtp和github.com/jordan-wright/email实现邮件发送

下方的smtp.PlainAuth中的第一个参数是邮箱账号,第二个是授权码

time

1、将时间戳转换为标准时间,如图:

运行结果如下:

2、获取当前时间,如图:

标签