Go 学习笔记

Go 学习相关笔记

Go 官方的教学文档顺序不怎么友好,这里根据我自己的学习曲线来记录文档的查看顺序

基础知识

文档预备

  1. 新手先要看 Go 的模块管理介绍,这样才知道基础 Go 怎么导入外部包和进行本地的包管理
    https://go.dev/doc/modules/managing-dependencies

上面的文章提到了怎么设置 Go 的包下载代理服务器!!!很重要的知识点。
https://go.dev/doc/modules/managing-dependencies#proxy_server

设置 Go 的国内代理:

# -w 作用是覆盖默认的设置
go env -w GOPROXY='https://goproxy.cn,direct'

这个包管理介绍的核心知识点:

  • 使用 go mod init 初始化出一个 module
  • 使用 go mod edit -replace example.com/greetings=../greetings 来建立本地模块的导入
    关系
  • 代码里面 import 了相应的包之后,使用 go mod tidy 来让 go 自动建立依赖

文档没有提及的知识点:

  • 一个文件夹内只可以放同一个 package 的代码文件
  1. 看完依赖管理后可以走一遍简单教程
    https://go.dev/doc/tutorial/

  2. 完成简单教程后就可以进入正式的教程深入学习
    https://go.dev/tour/welcome/1

走完上面三步,开发简单的项目就没什么问题了

Go 的 import 干了什么

强烈建议观看下面这篇博文,博主深入研究了 Go 在 import 一个包时的细节,这些都是官方文档没有提及的。
(不得不吐槽官方文档呀,新手教程写得很好,但是对于需要深入学习原理的很不友好。有些名著如《Go程序设计语言》应该
会提及,但是看名著需要大量的时间)

https://tonybai.com/2015/03/09/understanding-import-packages/

总结上面的博文(强烈建议去看,建议!建议!建议!)

下面四句话出自原文:

  1. 在使用第三方包的时候,当源码和.a均已安装的情况下,编译器链接的是源码。
  2. 到这里我们明白了所谓的使用第三方包源码,实际上是链接了以该最新源码编译的临时目录下的.a文件而已。
  3. import后面的最后一个元素应该是路径,就是目录,并非包名。
  4. m指代的是lib/math路径下唯一的那个包。
import 导入别名
Import declaration          Local name of Sin

import   "lib/math"         math.Sin
import m "lib/math"         m.Sin
import . "lib/math"         Sin

详细看文档
https://go.dev/ref/spec#Import_declarations

vscode 的智能行为

安装了 Go 插件的 Vscode, 会自动扫描出一个路径下面的唯一的包名,然后对包名和目录名不一致的 import
语句进行修改,使用和包名一样的别名引用相关包。

类似上面这个语句:

import m "lib/math"

vscode 搭建 Go 的开发环境

  • 安装 Go 的插件
    在这里插入图片描述

  • 安装 dlv 工具支撑 Go 在 vscode debug
    https://github.com/microsoft/vscode-go/blob/master/docs/Debugging-Go-code-using-VS-Code.md

数组

Go 传递数组是按值传递的,并不是像 C 语言那样传递首元素指针

  • 初始化固定长度的数组
b := [2]string{"Penn", "Teller"}
  • 让编译器自动计算数组长度
b := [...]string{"Penn", "Teller"}

切片

初始化一个切片(Slice), 和数组区别是切片的初始化表达式里不需要指定长度, 留意动态长度数组初始化和
切片初始化也是不一样的

letters := []string{"a", "b", "c", "d"}
  • 切片也可以用 make 函数来创建
func make([]T, len, cap) []T

对数组或者一个切片用坐标进行切分也会创建一个切片数据结构

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b

切片底层
在这里插入图片描述

len 是切片相对 ptr 引用的元素数量, cap 是底层数组相对 ptr 的元素数量。

扩大切片容量的基本原理就是新建一个具有更大空间的切片,然后将旧切片的数据拷贝到新的切片中。

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

官方提供了 append 函数来扩大一个切片

func append(s []T, x ...T) []T

展开运算符 …

暂时只在 slice 细节介绍博客里面提到展开运算符
https://go.dev/blog/slices-intro

  1. 收集参数到一个切片中
func append(s []T, x ...T) []T
  1. 传递参数时展开一个切片作为参数列表
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

type 建立类型别名

type rune = int32

type any = interface{}

type comparable interface{ comparable }

函数

参数列表
func add(x int, y int) int {
	return x + y
}

// 如果多个参数的类型一致,也可以写成
func add(x, y int) int {
	return x + y
}
返回多个结果以及接收多个结果

返回的类型在参数列表后面指定

func swap(x, y string) (string, string) {
	return y, x
}

func main() {
	a, b := swap("hello", "world")
	fmt.Println(a, b)
}
命名返回的结果

如果对返回的结果不单指定了类型,还提供了名称,那么同名的变量也会被创建。
当函数 return 语句后面为空时,将返回符合命名的变量数据

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
    // 等价于 return x, y
}

变量

声明

var 关键字可以声明变量列表,可以在全局和函数中声明

package main

import "fmt"

var c, python, java bool

func main() {
	var i int
	fmt.Println(i, c, python, java)
}
初始化

如果显示给出了初始化的值,则可以省略变量的类型声明,编辑器会自动推导类型,复杂类型还是显示写出
类型比较好

package main

import "fmt"

var i, j int = 1, 2

func main() {
	var c, python, java = true, false, "no!"
	fmt.Println(i, j, c, python, java)
}

当在函数内部声明变量时,使用特殊的赋值符号 := 可以省略 var 关键字,这个操作会在初始化变量
的同时推导其数据类型

但是全局的语句必须以关键字开头,所以全局变量的声明必须带上 var 关键字,无法使用 :=

基本类型

go 内置的基本数据类型

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
     // represents a Unicode code point

float32 float64

complex64 complex128
缺省默认值

变量声明时没有初始化时,会被赋予相应类型的缺省值

0 for numeric types,
false for the boolean type, and
"" (the empty string) for strings.

(nil for slice)
类型转换

T(v) 将值 v 转换到 类型 T

Go 类型转换都需要显示写出转换函数

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
常量

使用 const 关键字声明一个常量

const Pi = 3.14

流程控制

for 循环

go 只有一个循环语句 for

用分号分隔 初始语句; 条件语句; 循环后语句

初始语句 和 循环后语句 可以省略

func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	fmt.Println(sum)
}

类 C 的 while 语句写法, 移除分号

func main() {
	sum := 1
	for sum < 1000 {
		sum += sum
	}
	fmt.Println(sum)
}

无限循环

func main() {
	for {
	}
}

if

if 语句可以在进行判断前执行一条语句, 如果在这条语句中创建了变量,那么变量的将仅在这个 if
块内可见

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	}
	fmt.Println(lim)
	return lim
}

switch…case

go 的 switch...case 语句可以像 if 那样在进行判断前执行一条语句。而且 go 的 switch...case
和 C 语言的 switch...case 的区别是不需要在每个 case 语句末尾添加显示的 break

func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		// freebsd, openbsd,
		// plan9, windows...
		fmt.Printf("%s.\n", os)
	}
}

go 的 switch...case 不一定是要常量或者整数,可以是其他的值,具体细节这里不去展开

func main() {
   var x interface{}
     
   switch i := x.(type) {
      case nil:  
         fmt.Printf(" x 的类型 :%T",i)                
      case int:  
         fmt.Printf("x 是 int 型")                      
      case float64:
         fmt.Printf("x 是 float64 型")          
      case func(int) float64:
         fmt.Printf("x 是 func(int) 型")                      
      case bool, string:
         fmt.Printf("x 是 bool 或 string 型" )      
      default:
         fmt.Printf("未知型")    
   }  
}

没有条件语句的 switch 等效于 switch true,此时的 case 可以加入判断语句。
这种写法相当于写一段长的 if-else 语句

func main() {
	t := time.Now()
	fmt.Println(t.Hour())
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
}

defer

defer 语句将推迟一个函数的执行直到当前的函数块 return 之后,常常用来完成清理现场的工作,
注意函数的控制权也是在所有的 defer 调用完毕后才会移交给上级函数

func main() {
	fmt.Println("counting")

	for i := 0; i < 10; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("done")
}

多个 defer 语句按照后进先出的顺序执行 (栈式调用)

defer 推迟的函数调用中,传递的函数的参数是已经完成了求值,并不会受到后续语句的影响。

defer 的函数调用可能会读取或者修改所在调用函数块的命名返回变量,这种特性是为了方便修改错误的
函数返回值

defer panic recover

当一个函数 F 调用了 panic,F 将停止执行,然后按照栈顺序将所有的 defer 的函数,然后执行权移交给 F 的调用函数 G,并且此时 F 的执行效果等价于执行了 panic。一个函数中如果调用了 recover,将会取到此时 panic 传入的值,
然后将当前函数从 panic 状态恢复到正常执行的状态。因为 recover 只能在 defer 函数里调用,等同于只要执行过 recover,后续的 defer 执行完后,
该函数会正常结束,不会再到调用函数中触发 panic。如果在没有 panic 的函数内调用 recover,返回值是 nil

指针

指针类型声明

var p *int

和 C 语言类似的取指针操作

i := 42
p = &i

取指针的值

fmt.Println(*p) // read i through the pointer p
*p = 21         // set i through the pointer p

go 不能像 C 语言那样对指针进行算术运算

结构

声明一个结构

type Vertex struct {
	X int
	Y int
}

使用 . 访问结构成员

func main() {
	v := Vertex{1, 2}
	v.X = 4
	fmt.Println(v.X)
}

结构指针也可以通过 . 访问成员

func main() {
	v := Vertex{1, 2}
	p := &v
	p.X = 1e9
	// 或者 (*p).X
	fmt.Println(v)
}

结构字面量

字面量书写的顺序和结构成员的声明的顺序保持一致;
可以通过命名成员的方式初始化一个结构,此时成员的初始值和书写顺序无关;
若没有显示写出初始值,各个成员的初始值将会隐示指定;
可以用 & 仅返回初始化的结构的指针;

var (
	v1 = Vertex{1, 2}  // has type Vertex
	v2 = Vertex{X: 1}  // Y:0 is implicit
	v3 = Vertex{}      // X:0 and Y:0
	p  = &Vertex{1, 2} // has type *Vertex
)

func main() {
	fmt.Println(v1, p, v2, v3)
}

Range

range 形式的 for 循环可以遍历一个 slice 或者 map

range 每次迭代返回两个值,第一个是下标,第二个是该下标对应的值

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
	for i, v := range pow {
		fmt.Printf("2**%d = %d\n", i, v)
	}
}

可以使用 _ 来忽略某个位置的值的赋值

for i, _ := range pow
for _, value := range pow

也可以只使用第一个数值

for i := range pow

Maps

maps 建立一个键值对

可以通过 make 函数来创建指定类型的 maps

package main

import "fmt"

type Vertex struct {
	Lat, Long float64
}

var m map[string]Vertex

func main() {
	m = make(map[string]Vertex)
	m["Bell Labs"] = Vertex{
		40.68433, -74.39967,
	}
	fmt.Println(m["Bell Labs"])
}

字面量

可以用字面量来创建并初始化一个 maps

package main

import "fmt"

type Vertex struct {
	Lat, Long float64
}

var m = map[string]Vertex{
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}

func main() {
	fmt.Println(m)
}

maps 的操作

插入新的键值对

m[key] = elem

获取值

elem = m[key]

删除键

delete(m, key)

判断键是否在 maps

elem, ok = m[key]

keym 中时,oktrue, 反之为 false
okfalse 时,elem 就会是对应类型的零值

注意 elemok 没有提前声明时可以使用赋值声明的方式

elem, ok := m[key]

函数

go 里面的函数也是一种数值,意味着可以当成参数传递给另一个函数或者作为函数的返回值

package main

import (
	"fmt"
	"math"
)

func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}

func main() {
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	fmt.Println(hypot(5, 12))

	fmt.Println(compute(hypot))
	fmt.Println(compute(math.Pow))
}

方法与类型

go 没有类的概念,但是可以定义类型上的方法

在函数 func 关键字和函数名之间添加一个特殊的 receiver 参数,就可以在指定类型
上定义一个方法

以下示例在 Vertex 上定义了一个 Abs 方法

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}

类型的方法只能在与类型定义相同的包中定义,也就是不能定义另一个包的类型上面的方法

定义类型方法时使用类型指针

如果定义类型方法时 receiver 是一个类型指针,那么这个方法就可以修改到原始类型内部的值。如果只是传递类型,方法内部对结构上的值的修改不会影响原始结构。

指针传递可以避免结构值的拷贝,提升效率

在类型方法的定义中,指针传递和值传递影响的是 interface 的方法检测行为,参考下节

Interfaces

一组方法的签名可以定义为一个 interface 类型

只要实现了相应的方法签名,一个值就可以认为是匹配的 interface 类型

下面的示例说明,在 *Vertex 上实现了 Abs 方法时,只有 Vertex 的指针可以赋值给 Abser 类型,单纯的 Vertex 赋值给 Abser 类型会报错。

package main

import (
	"fmt"
	"math"
)

type Abser interface {
	Abs() float64
}

func main() {
	var a Abser
	f := MyFloat(-math.Sqrt2)
	v := Vertex{3, 4}

	a = f  // a MyFloat implements Abser
	a = &v // a *Vertex implements Abser

	// In the following line, v is a Vertex (not *Vertex)
	// and does NOT implement Abser.
	a = v

	fmt.Println(a.Abs())
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

未初始化的类型值

一个类型没有初始化时,其值一般为 nil, 此时定义的类型方法中接收到的 receiver 也是 nil

建议实现类型方法时考虑 nil 的情形

package main

import "fmt"

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}

func main() {
	var i I

	var t *T
	i = t
	describe(i)
	i.M()

	i = &T{"hello"}
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

没有初始化 interface 类型的变量就调用其方法,会报错

空的 interface

interface{} 是一个没有方法签名的,空的 interface 类型,其意义是表示任意一种类型。常用在需要接受未知类型参数的方法中。

底层

interface 底层可以视为一个 (value, type) 的元组

类型断言

t := i.(T) 用来断言 i 是一个 T 类型,并将转换成功的 i 值赋值给 t
如果断言失败,上面的代码就会触发一个 panic

可以通过接受两个返回值来规避 panic

t, ok := i.(T)

断言成功,t 就是转换成功的值,oktrue
断言失败,t 为零值,okfalse

type switch

type switch 是一种代码结构, 常用来组织多个类型断言, 同时 default 分支的存在
也可以规避 panic 的触发

package main

import "fmt"

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

下面脱离了 switch 的类型断言会触发一个 panic

package main

import "fmt"

func do(i interface{}) {
	a := i.(int)
	fmt.Printf("%v, %T", a, a)
}

func main() {
	do(21)
	do("hello")
	do(true)
}

内置 interface

Stringer

fmt 打印值时会寻找下面的接口

String 方法返回一个描述自身的字符串

type Stringer interface {
    String() string
}
Errors
type error interface {
    Error() string
}
io.Reader

实现以下方法的类型就是一个 Reader

func (T) Read(b []byte) (n int, err error)

该方法接收一个 byte 类型的切片,填充读到的数据到这个切片中,然后返回填充的数量和一个 error 表示是否读取结束(io.EOF

package main

import (
	"fmt"
	"io"
	"strings"
)

func main() {
	r := strings.NewReader("Hello, Reader!")

	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}

类型参数

表示一个函数接收的类型需要满足 comparable 约束,comparable 是一个 interface

func Index[T comparable](s []T, x T) int

泛型声明

package main

// List represents a singly-linked list that holds
// values of any type.
type List[T any] struct {
	next *List[T]
	val  T
}

func main() {
}

并发

一个 goroutine 是由 Go 运行时管理的一种轻量线程。

goroutine 视为协程是因为它具有协程的核心特点——在协程之间的调度不需要涉及任何系统调用或任何阻塞调用。
一般的线程是需要经由操作系统来进行调度,底层涉及到了各种同步性原语,如互斥锁,信号量等。但是
goroutine 是完全由 Go 运行时管理。

go f(x, y, z)

上面会启动一个 goroutine

goroutine 和主程序共享内存地质空间,所以 goroutine 对内存的访问需要同步进行,sync
包提供了同步支持。

Channels

Channels 是一种带有类型的管道,你可以通过它来传递和接收数据,操作符是 <-

ch <- v    // Send v to channel ch.
v := <-ch  // Receive from ch, and
           // assign value to v.

Channels 在使用前也需要声明

ch := make(chan int)

默认情况下,Channels 在传递和接收数据时都会阻塞等待其中一边准备就绪,这个特点可以用来在
goroutine 之间进行数据同步

package main

import "fmt"

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // send sum to c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // receive from c

	fmt.Println(x, y, x+y)
}

Channels 可以在声明时指定缓存大小,这样如果往 Channels 发送数据,就会在 Channels
缓存满时阻塞。同理,从 Channels 接收数据时,会在缓存空时阻塞。

ch := make(chan int, 100)

Range 和 Close

Channels 的发送者可以关闭一个 Channels,此时 Channels 的接收者可以通过接收两个
参数来获取关闭状态

v, ok := <-ch

如果 Channels 没有更多的数据并且 Channels 被关闭了的话,ok 就为 false

下面的循环会不断的从 Channels c 中读取数据直至它被关闭

for i := range c
package main

import (
	"fmt"
)

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
		fmt.Println(i)
	}
}

Select

使用 selectChannels 进行结合,可以写出强大的多条件同步判断。

select 会对所有的 case 进行阻塞,直到其中一个接收到值为止,如果此时有多个 case 准备就绪,
select 会随机地选择一个进行执行

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
		
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

注意,一般不要写 selectdefault 情况,否则 default 的分支会被执行多次。

部分可以使用 default 的示例:

package main

import (
	"fmt"
	"time"
)

func main() {
	tick := time.Tick(100 * time.Millisecond)
	boom := time.After(500 * time.Millisecond)
	for {
		select {
		case <-tick:
			fmt.Println("tick.")
		case <-boom:
			fmt.Println("BOOM!")
			return
		default:
			fmt.Println("    .")
			time.Sleep(50 * time.Millisecond)
		}
	}
}

互斥锁

Go 也提供了互斥的锁的相关数据类型 sync.Mutex, 其常用方法是 LockUnlock
也可以结合 defer 的使用来确保锁的释放。

package main

import (
	"fmt"
	"sync"
	"time"
)

// SafeCounter is safe to use concurrently.
type SafeCounter struct {
	mu sync.Mutex
	v  map[string]int
}

// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
	c.mu.Lock()
	// Lock so only one goroutine at a time can access the map c.v.
	c.v[key]++
	c.mu.Unlock()
}

// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
	c.mu.Lock()
	// Lock so only one goroutine at a time can access the map c.v.
	defer c.mu.Unlock()
	return c.v[key]
}

func main() {
	c := SafeCounter{v: make(map[string]int)}
	for i := 0; i < 1000; i++ {
		go c.Inc("somekey")
	}

	time.Sleep(time.Second)
	fmt.Println(c.Value("somekey"))
}

WaitGroup

当程序需要启动多个 goroutine, 比如多个接口请求并发,就可以使用 sync.WaitGroup 来进行管理,简单使用代码参考下面的样例。

sync 包提供了很多线程同步相关的原语操作,文档地址:
https://pkg.go.dev/sync#WaitGroup

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int) {
    fmt.Printf("Worker %d starting\n", id)

    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {

    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)

        go func() {
            defer wg.Done()
            worker(i)
        }()
    }

    wg.Wait()

}

测试相关

Go 官方对代码测试提供了非常完善的支持,结合现代的 IDE, 如 Vscode 或者 JetBrain 的 GoLand, 能够让开发体验更上一层楼。

编写测试的方法很简单,首先新建一个文件, 命名为 xxxx_test.go,只要是 _test.go 后缀,Go 就会自动扫描并提供语法支持。

接着在这个文件里面可以导出任意以 Test 开头的函数,并且导出的函数接收一个 testing.T 的指针,即可让 Go 导入相关的测试依赖。Go 的包通过函数首字母的大小写来判断这个函数是否可以被外部导入来使用。

函数签名如下:

func (*testing.T)

在这个函数里面导入你想要测试的包和其他相关测试代码,然后输出日志记得使用传入的测试参数对象就行。

下面的代码是一个测试 go-resty 相关功能的样例:

// 文件名:goresty_test.go
package gorestytest_test

import (
	"testing"

	"github.com/go-resty/resty/v2"
)

func TestGoresty(t *testing.T) {
	client := resty.New()
	client.SetProxy("http://127.0.0.1:8889")
	resp, err := client.R().EnableTrace().Get("https://example.com/")

	// Explore response object
	t.Log("Response Info:")
	t.Log("  Error      :", err)
	t.Log("  Status Code:", resp.StatusCode())
	t.Log("  Status     :", resp.Status())
	t.Log("  Proto      :", resp.Proto())
	t.Log("  Time       :", resp.Time())
	t.Log("  Received At:", resp.ReceivedAt())
	t.Log("  Body       :\n", resp)
	t.Log()

	// Explore trace info
	t.Log("Request Trace Info:")
	ti := resp.Request.TraceInfo()
	t.Log("  DNSLookup     :", ti.DNSLookup)
	t.Log("  ConnTime      :", ti.ConnTime)
	t.Log("  TCPConnTime   :", ti.TCPConnTime)
	t.Log("  TLSHandshake  :", ti.TLSHandshake)
	t.Log("  ServerTime    :", ti.ServerTime)
	t.Log("  ResponseTime  :", ti.ResponseTime)
	t.Log("  TotalTime     :", ti.TotalTime)
	t.Log("  IsConnReused  :", ti.IsConnReused)
	t.Log("  IsConnWasIdle :", ti.IsConnWasIdle)
	t.Log("  ConnIdleTime  :", ti.ConnIdleTime)
}

如果用 vscode 来打开这个文件,可以看到他自动添加了相关的功能按钮在函数上,这样无论是直接运行代码还是 debug 都相当的方便(这些功能按钮是 Go 语言的插件提供的,可以回到文章开头来找插件连接)。

在这里插入图片描述

如果不借助这个测试机制,就需要编写相应的 IDE 调试文件才能调试 Go 的代码。

这里给出 vscode 调试 Go 的 launch.json 配置:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch Package",
            "type": "go",
            "request": "launch",
            "mode": "auto",
            "program": "${fileDirname}"
        }
    ]
}
  • 18
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
GoLang学习笔记主要包括以下几个方面: 1. 语法规则:Go语言要求按照语法规则编写代码,例如变量声明、函数定义、控制结构等。如果程序中违反了语法规则,编译器会报错。 2. 注释:Go语言中的注释有两种形式,分别是行注释和块注释。行注释使用`//`开头,块注释使用`/*`开头,`*/`结尾。注释可以提高代码的可读性。 3. 规范代码的使用:包括正确的缩进和空白、注释风格、运算符两边加空格等。同时,Go语言的代码风格推荐使用行注释进行注释整个方法和语句。 4. 常用数据结构:如数组、切片、字符串、映射(map)等。可以使用for range遍历这些数据结构。 5. 循环结构:Go语言支持常见的循环结构,如for循环、while循环等。 6. 函数:Go语言中的函数使用`func`关键字定义,可以有参数和返回值。函数可以提高代码的重用性。 7. 指针:Go语言中的指针是一种特殊的变量,它存储的是另一个变量的内存地址。指针可以实现动态内存分配和引用类型。 8. 并发编程:Go语言提供了goroutine和channel两个并发编程的基本单位,可以方便地实现多线程和高并发程序。 9. 标准库:Go语言提供了丰富的标准库,涵盖了网络编程、文件操作、加密解密等多个领域,可以帮助开发者快速实现各种功能。 10. 错误处理:Go语言中的错误处理使用`defer`和`panic`两个关键字实现,可以有效地处理程序运行过程中出现的错误。 通过以上内容的学习,可以掌握Go语言的基本语法和编程思想,为进一步学习和应用Go语言打下坚实的基础。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Golang学习笔记](https://blog.csdn.net/weixin_52310067/article/details/129467041)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [golang学习笔记](https://blog.csdn.net/qq_44336275/article/details/111143767)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值