Go 学习笔记

Go 学习相关笔记

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

基础知识

文档预备

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

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

  • 使用 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 Sinimport   "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 = int32type 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 / 9y = sum - xreturn// 等价于 return x, y
}

变量

声明

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

package mainimport "fmt"var c, python, java boolfunc main() {var i intfmt.Println(i, c, python, java)
}
初始化

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

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

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

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

基本类型

go 内置的基本数据类型

boolstringint  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptrbyte // alias for uint8rune // alias for int32// represents a Unicode code pointfloat32 float64complex64 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 := 0for i := 0; i < 10; i++ {sum += i}fmt.Println(sum)
}

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

func main() {sum := 1for 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 intY int
}

使用 . 访问结构成员

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

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

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

结构字面量

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

var (v1 = Vertex{1, 2}  // has type Vertexv2 = Vertex{X: 1}  // Y:0 is implicitv3 = Vertex{}      // X:0 and Y:0p  = &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 mainimport "fmt"type Vertex struct {Lat, Long float64
}var m map[string]Vertexfunc main() {m = make(map[string]Vertex)m["Bell Labs"] = Vertex{40.68433, -74.39967,}fmt.Println(m["Bell Labs"])
}

字面量

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

package mainimport "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 mainimport ("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 mainimport ("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 mainimport ("fmt""math"
)type Abser interface {Abs() float64
}func main() {var a Abserf := MyFloat(-math.Sqrt2)v := Vertex{3, 4}a = f  // a MyFloat implements Absera = &v // a *Vertex implements Abser// In the following line, v is a Vertex (not *Vertex)// and does NOT implement Abser.a = vfmt.Println(a.Abs())
}type MyFloat float64func (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 mainimport "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 Ivar t *Ti = tdescribe(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 mainimport "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 mainimport "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 mainimport ("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 mainimport "fmt"func sum(s []int, c chan int) {sum := 0for _, 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 cfmt.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 mainimport ("fmt"
)func fibonacci(n int, c chan int) {x, y := 0, 1for i := 0; i < n; i++ {c <- xx, 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, 1for {select {case c <- x:x, y = y, x+ycase <-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 mainimport ("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!")returndefault:fmt.Println("    .")time.Sleep(50 * time.Millisecond)}}
}

互斥锁

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

package mainimport ("fmt""sync""time"
)// SafeCounter is safe to use concurrently.
type SafeCounter struct {mu sync.Mutexv  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 mainimport ("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.WaitGroupfor 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_testimport ("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 objectt.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 infot.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}"}]
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/317144.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【快速入门】数据库的增删改查与结构讲解

文章的操作都是基于小皮php study的MySQL5.7.26进行演示 what 数据库是能长期存储在计算机内&#xff0c;有组织的&#xff0c;可共享的大量数据的集合。数据库中的数据按照一定的数据模型存储&#xff0c;具有较小的冗余性&#xff0c;较高的独立性和易扩展性&#xff0c;并为…

本地CPU搭建知识库大模型来体验学习Prompt Engineering/RAG/Agent/Text2sql

目录 1.环境 2.效果 3.概念解析 4.架构图 5. AI畅想 6.涉及到的技术方案 7. db-gpt的提示词 1.环境 基于一台16c 32G的纯CPU的机器来搭建 纯docker 打造 2.效果 3.概念解析 Prompt Engineering &#xff1a; 提示词工程 RAG&#xff1a; 检索增强生成&#xff1b; …

CTFHub-Web-SQL注入

CTFHub-SQL注入-WP 1.整数型注入 1.题目说输入1&#xff0c;先将1输入查看结果 2.接着输入4-1&#xff0c;发现输出的结果为4-1&#xff0c;判定存在整数型注入 3.查询字段数&#xff0c;出现了回显&#xff0c;判断这里的字段数为2 1 order by 24.判断注入点在2的位置&…

复杂度(3)

目录 1.二分查找的时间复杂度 2.斐波那契数列及其优化 3.空间复杂度 1.二分查找的时间复杂度 我们熟知的二分查找绝对是一种很厉害的算法&#xff0c;因为这个算法每进行一次都会砍掉一半的数据&#xff0c;相当于是指数级增长&#xff0c;假设我们刚开始的时候数据的个数是…

MS8241/MS8242高速、高输出电流、电压反馈放大器

产品简述 MS8241/MS8242 是一颗高速的电压反馈放大器&#xff0c;具有电流 反馈放大器的高速转换特性&#xff0c;可以应用在所有传统的电压反馈运 放应用方案中。 MS8241/MS8242 能够稳定工作在低增益环路下 &#xff08;增益为 2 和 -1 &#xff09;&#xff0c;仅消耗…

Java项目:基于SSM框架实现的实践项目管理系统(ssm+B/S架构+源码+数据库+毕业论文+开题报告)

一、项目简介 本项目是一套基于SSM框架实现的实践项目管理系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff…

【算法】【贪心算法】【leetcode】870. 优势洗牌

题目地址&#xff1a;https://leetcode.cn/problems/advantage-shuffle/description/ 题目描述&#xff1a; 给定两个长度相等的数组 nums1 和 nums2&#xff0c;nums1 相对于 nums2 的优势可以用满足 nums1[i] > nums2[i] 的索引 i 的数目来描述。 返回 nums1 的任意排列&…

HTML5(1)

目录 一.HTML5(超文本&#xff08;链接&#xff09;标记&#xff08;标签<>&#xff09;语言) 1.开发环境&#xff08;写代码&#xff0c;看效果&#xff09; 2.vscode 使用 3.谷歌浏览器使用 4.标签语法 5.HTML基本骨架&#xff08;网页模板&#xff09; 6.标签的…

【配置】Docker搭建JSON在线解析网站

云服务器打开端口8787 连接上docker运行 docker run -id --name jsonhero -p 8787:8787 -e SESSION_SECRETabc123 henryclw/jsonhero-webhttp://ip:8787访问 Github&#xff1a;地址

透视天气:数据可视化的新视角

数据可视化在天气方面能够为我们带来极大的帮助。天气是人类生活中一个重要的因素&#xff0c;对于农业、交通、航空、能源等各个领域都有着重要的影响。而数据可视化技术通过将复杂的天气数据转化为直观、易懂的图表、图像或地图等形式&#xff0c;为我们提供了更深入、更全面…

接口测试 - postman

文章目录 一、接口1.接口的类型2. 接口测试3. 接口测试流程4. 接口测试用例1. 测试用例单接口测试用例-登录案例 二、HTTP协议1. HTTP请求2. HTTP响应 三、postman1. 界面导航说明导入 导出用例集 Get请求和Post请求的区别:2.postman环境变量和全局变量3. postman 请求前置脚本…

ECharts在网页中添加可视化图标-在网页中添加交互图表+option模块案列详解

一、引言 ECharts 是一个使用 JavaScript 编写的开源可视化库&#xff0c;它可以在浏览器中生成交互式的图表。无论是折线图、柱状图、散点图还是饼图&#xff0c;ECharts 都能轻松应对。本文将带领大家了解如何在网页中添加 ECharts 可视化图标。 本章可以直接跳到第五点完整…

【Spring基础】关于Spring IoC的那些事

文章目录 一、如何理解IoC1.1 Spring IOC 概述1.2 IoC 是什么 二、Ioc 配置的方式2.1 xml 配置2.2 Java 配置2.3 注解配置 三、依赖注入的方式3.1 setter方式3.2 构造函数3.3 注解注入 小结 一、如何理解IoC 1.1 Spring IOC 概述 控制反转 IoC(Inversion of Control)是一种设计…

2024数学建模时间汇总与竞赛攻略

目录 2024数学建模汇总&#xff08;时间、报名费、获奖率、竞赛级别、是否可跨校&#xff09; 中国高校大数据挑战赛 “华数杯”国际大学生数学建模竞赛 美国大学生数学建模竞赛&#xff08;美赛&#xff09; 数学中国&#xff08;认证杯&#xff09;数学建模网络挑战赛 …

解决RuntimeError: cuDNN error: CUDNN_STATUS_EXECUTION_FAILED

下图说明在一瞬间我的GPU就被占满了 我的模型在训练过程中遇到了 CUDA 相关的错误&#xff0c;这是由于 GPU资源问题或内存不足导致的。这类错误有时候也可能是由于某些硬件兼容性问题或驱动程序问题引起的。 为了解决这个问题&#xff0c;可以尝试以下几个解决方案&#xff1a…

【Godot4.2】有序和无序列表函数库 - myList

概述 在打印输出或其他地方可能需要构建有序或无序列表。本质就是构造和维护一个纯文本数组。并用格式化文本形式&#xff0c;输出带序号或前缀字符的多行文本。 为此我专门设计了一个类myList&#xff0c;来完成这项任务。 代码 以下是myList类的完整代码&#xff1a; # …

模型剪枝-Network Slimming算法分析

代码见文末 论文地址&#xff1a;Learning Efficient Convolutional Networks through Network Slimming ICCV 2017 Open Access Repository 1.概述 由于边缘设备的限制&#xff0c;在模型的部署中经常受到模型大小、运行内存、计算量的限制。之前的方法要么只能解决其中一个…

spark实验求TOP值

实验1&#xff1a;求TOP值 已知存在两个文本文件&#xff0c;file1.txt和file2.txt&#xff0c;内容分别如下&#xff1a; file1.txt 1,1768,50,155 2,1218, 600,211 3,2239,788,242 4,3101,28,599 5,4899,290,129 6,3110,54,1201 7,4436,259,877 8,2369,7890,27 fil…

合泰杯(HT32F52352)RTC的应用(计时)--->掉电不丢失VBAT(代码已经实现附带源码)

摘要 在HT32F52352合泰单片机开发中&#xff0c;rtc在网上还是挺少人应用的&#xff0c;找了很久没什么资料&#xff0c;现在我根据手册和官方的代码进行配置理解。 RTC在嵌入式单片机中是一个很重要的应用资源。 记录事件时间戳&#xff1a;RTC可以记录事件发生的精确时间&…

STL——stackqueue

stack stack即为栈&#xff0c;先进后出是其特点 栈只有栈顶元素能被外界使用&#xff0c;故不存在遍历行为 栈中常用接口 构造函数 stack<T> stk; //默认构造方式 stack(const stack &stk); //拷贝构造 赋值操作 stack& operator(const stack &stk); …