Golang学习笔记

Golang笔记

go mod相关

使不使用module的区别

  • 开启mod会将包下载到gopath下的pkg下的mod文件夹中
  • 关闭mod会将包下载到gopath的src下
  • auto模式只有当当前目录在GOPATH/src目录之外且当前目录包含go.mod文件或子目录包含go.mod文件才会启用
  • 项目可以不用创建在src下了,任何非中文路径下都可以

使用mod步骤

  • 开启mod:go111module=on
  • 进入项目,执行go mod init (在项目根目录生成go.mod文件)
  • 启动项目(go.mod添加依赖包)

go mod命令

  • download 下载模块到本地缓存,go env中的GOCACHE路径,可以通过go clean -cache清空缓存,多个项目可以共享缓存的包
  • edit 在工具或脚本中编辑go.mod文件
  • graph 打印模块需求图
  • init 在当前目录下初始化新的模块,go mod init [项目名]
  • tidy 添加缺失的模块以及移除无用的模块,生成go.sum文件
  • vendor 自动下载项目中依赖包到项目根目录下的vendor文件夹,并写入go.mod文件同时生成modules.txt文件
  • verify 检查当前模块依赖是否全部下载,是否下载下来被修改过
  • why 解释为什么需要包或者模块

go vendor

  • 将引用的外部包的源代码放在当前工程的vendor目录下面

  • 编译go代码会优先从vendor目录先寻找依赖包

  • 有了vendor目录后,打包当前的工程代码到其他机器的$GOPATH/src下都可以通过编译

  • 如果要指定特定版本的外部包 需要去go.mod重新编写并重新执行go vendor

      使用go-mod以后,项目可以放在任何非中文路径下,gopath必须放在gopath的src下
    

内置类型

25个关键字

在这里插入图片描述

  • fallthrough 在switch case中使用 继续进行下一个case判断

定义全局变量的方式

  • var Name string
  • var Name string = “abc”
  • var Name = “abc”

定义局部变量的方式

  • var i int = 10
  • var i = 10
  • i := 10

拼接字符串的方式

  • “a” + “b”
  • str := fmt.Sprintf(%s%s, “a”, “b”)

数组和切片的区别

  • 数组是值类型,切片是引用
  • 数组长度已知,切片长度可变

go语言局部变量分配在栈还是堆

go语言编译器会自动决定,当发现变量的作用域没有跑出函数的范围就在栈上,反之则堆

slice

底层是数组,保存了len,capacity和对数组的引用

函数

init

  • init函数是用于程序执行前做包的初始化的函数
  • 每个包可以拥有多个init函数
  • 包的每个源文件也可以拥有多个init函数
  • 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序
  • init函数不能被其他函数调用,而是在main函数执行之前自动被调用

copy

copy(dst,src)

  • 不同类型的切片无法复制
  • 如果dst长度大于src,将dst中对应位置的值替换成src对应位置的值
  • 如果dst长度小于src,多余的不做替换

delete

从map中删除key对应的value

	map_data := map[string]interface{}{"name":"a","age":19}
	delete(map_data, "name")

make

用来分配内存,只能应用于slice,map,chan,返回Type

new

用来分配内存,主要用来分配值类型,参数是一个类型,返回*Type
它返回的永远是类型的指正,指向分配类型的内存地址,并且内存置为零值

闭包

当使用闭包方式访问某个局部变量时,该局部变量会常驻内存,访问速度会特别快,并且我们始终操作的都是这个变量本身,而不是其副本,修改时也特别方便

  1. 定义
  • 闭包是匿名函数与匿名函数所引用环境的组合
  • 定义在一个函数内部的函数,闭包是将函数内部和函数外部连接起来的桥梁
  1. 特点
  • 让外部访问函数内部的变量成为可能
  • 局部变量会常驻在内存中
  • 可以避免使用全局变量,防止全局变量污染
  • 有一块内存空间被长期占用,而不被释放,会造成内存泄漏
  1. 什么情况下算闭包
  • 一个外部函数,一个内部函数
  • 内部函数使用了外部函数的变量
  • 外部函数返回的是内部函数
  1. 使用场景
    对某一类相同的操作进行

封装

通过约定来实现权限控制(首字母的大小写)
大写即为public 小写为private

继承


type MyBase struct {
	Id int
}

func (m *MyBase) Run {
	fmt.Println("run!")
}

type Student struct {
	base.MyBase
	score int
}

func (s *Student) Study() {
	fmt.Println("study!")
}

func main() {
	student := Student{score:100, MyBase:base.MyBase{Id:1}}
	/* 或者
	student := Student{score:100}
	student.Id = 1
	*/
	fmt.Println(student) // {{1} 100}
	student.Study() // study!
	student.Run() // run!
}

多态


type Animal interface {
	Say()
}

func AnimalSay(a *Animal) {
	a.Say()
}

type Cat struct {
	Name string
	Cry string
}

func (c *Cat) Say() {
	fmt.Printf("%s%s"c.Name,c.say)
}

type Dog struct {
	Name string
	Cry string
}

func (d *Dog) Say() {
	fmt.Printf("%s%s"c.Name,c.say)
}

func main {
	dog := Dog{Name:"dog", Cry:"wang"}
	AnimalSay(&dog)  // dogwang
	cat:= Cat{Name:"cat", Cry:"miao"}
	AnimalSay(&dog) // catmiao
}

interface

接口是go语言中所有数据机构的核心
所有类型都实现了空接口interface{},空接口可以用来做泛型
可以实现多态
是method的集合,也是一种类型,只有签名,没有具体实现代码

	type 接口名称 interface {
		方法
	}

接口的好处:

  • 泛型编程
  • 隐藏具体的实现
  • 传参:不同的结构体参数

###类型转换

type MyInt int
var mi MyInt
var i int = 1
var in interface{}
  • mi = MyInt(i)
  • mi = in.(MyInt)

错误和异常

避免异常的两种方式

  • defer中使用recover,使程序复活,defer必须事先定义,而不是发生在异常后面定义,执行顺序与声明顺序相反
defer func() {
	if err := recover(); err != nil {
		logs.Error("error %s", err)
		l.TplName = "succrss.html"
	}
}
  • 以返回错误代替异常
a := "aaa"
b,err := strconv.ParseInt(a, 10, 64)

并发

并行

  • 并行的关键是有同时处理多个任务的能力
  • 并行是加硬件可以解决的
  • 同一时间只能做一件事
  • 同一时刻,有多条指令在多个cpu上同时执行

并发

  • 并发的关键是有处理多个任务的能力,不一定要同时
  • 并发是代码性能优化可以解决的
  • 同一时间可以快速切换做多件事(同一时刻只能执行一个)
  • 同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行

并发和并行都可以是多线程,这些线程同时被多个cpu执行,那就是并行;这些线程同时被一个cpu轮流切换着执行,那就是并发

进程

  • 进程是资源分配和调度的最小单位
  • 动态性:进程是程序的一次执行过程,是临时的,有生命期的,动态产生的,动态消亡的
  • 并发性:任何进程都可以同其他进行一起并发执行
  • 独立性:进程是系统进行资源分配和调度的一个独立单位
  • 结构性:进程由陈旭,数据和进程控制块(pcb)三部分组成

线程

  • 线程是调用cpu运算的最小单位
  • 线程的调度由操作系统进行
  • 一个进程可以有多个线程

协程

  • 轻量级线程(go语言中为goroutine)
  • 协程的调度由用户控制

go中使用并发

  • goroutine必定对应一个函数
  • 多个goroutine可以执行相同的函数
  • Go程序会为main函数创建一个默认的goroutine

sync.WaitGroup的使用

sync:synchronization同步这个词的缩写,所以也叫同步包
WaitGroup:同步等待组,内部有一个计数器,从0开始,不能为负数

  • Add(delta int):设置计数器
  • Done():会把计数器-1
  • Wait():阻塞代码运行,直到计数器为0时终止
func testWait(wg *sync.WaitGroup) {
	fmt.Println("testWait run!")
	wg.Done()
}

func main() {
	var wg = sync.WaitGroup{}
	wg.Add(1)
	go testWait(&wg)
	wg.Wait()
}

chan的特性

  • 给一个空channel发送数据,造成永远堵塞
  • 给一个空channel接受数据,造成永远堵塞
  • 给一个已经关闭的channel发送数据,引起panic
  • 从一个已经关闭的channel接受数据,如果缓冲区为空,则返回一个零值
  • 无缓冲的channel是同步的,有缓冲的channel是非同步的

chan的使用

  1. 无缓存的chan
    ch1 := make(chan int)
    var ch1 chan int
    
    ch2 := make(<-chan int) // 只读通道
    var ch2 <- chan int
    
    ch3 := make(chan<- int)
    var ch3 chan <- int
    
  • 没有指定写或者读,默认是双向的
  • 把数据往通道中发送时,如果接收方一直没有接受,那么发送操作将持续堵塞
  1. 有缓存的chan
    ch1 := make(chan int, capacity) // capacity 容量int
    
  • 如果定义了缓存大小,写入大于了就会报错
  • 通过len函数可以获取chan中元素个数,cap得到channel的缓存长度
  • 当缓存满时,发送消息将堵塞
  • 当channel为空时,读取操作将堵塞
  1. 有缓存和无缓存的区别
  • 无缓存chan,chan为空,读取将堵塞,chan不为空,写入将堵塞
  • 有缓存chan,当chan空读取将堵塞,当缓存满了写入将堵塞

Goroutine池

作用
  • 大量的goroutine会占用大量的内存
  • 重复的创建与销毁goroutine,大量的goroutine调度会占用资源
  • 减少goroutine创建时间,提高效率
  • 管理协程,控制并发量,定期回收
实现思路
  • 启动服务器时初始化一个goroutine pool,维护任务的管道和worker
  • 外部将请求投递到pool,pool判断当前运行的worker是否超过了pool容量,超过啧放到任务管道中知道运行的worker将管道中的任务执行,否则就新开一个worker处理

select

  • 语句只能用于管道的读写操作
  • select可以同时监听多个channel的读写
  • 执行select时,若只有一个case通过(不阻塞),则执行这个case块
  • 若有多个case通过,则随机挑选一个case执行
  • 若所有case均阻塞,且定义了default模块,则执行default模块。若未定义default模块,则select语句阻塞,直到有case被唤醒
  • 使用break会跳出select块
select {
case data <- ch:
	fmt.Println(data)
case ch <- data:
	fmt.Println("写入成功")
case <- time.After(time.Second * 2):
	fmt.Println("操作超时")
}

读写锁

  • 写锁:写写互斥,写读互斥
  • 读锁:读读不互斥,读写互斥

单例(sync.Once)

type Person struct {
	Name string
}
var sonce sync,Once
var person *Person

func NewPersn(name string) *Person {
	sonce.Do(func() {
		person = new(Person)
		person.Name = name
	})
	return person
}

死锁

死锁产生的原因

  • 系统同资源的竞争导致资源不足,以及资源分配不当,导致死锁
  • 请求和释放资源的顺序不当,导致死锁

如何避免

  • channel推送和读取同时存在时才不会发生死锁,而且需要注意channel读写的顺序,只读或只写都会发生死锁

反射

使用场景

  • 做对象的序列化,构建适用于不同类型的工具
  • 序列化和反序列化,比如json,protobuf等各种数据协议
  • 各种数据库的ORM,比如gorm,sqlx等数据库的中间件
  • 配置文件解析相关的库,比如yami,ini等
  • 编写一个函数,但并不知道传给你的参数类型是什么,需要根据某些条件决定调用哪个函数,调用的函数是字符串参数

reflect.TypeOf

func test(i interface{}) {
	t := reflect.TypeOf(i)
	kind := t.Kind()
	switch kind {
	case reflect.Int:
		fmt.Println("int")
	case reflect.String:
		fmt.Println("string")
	}
}

reflect.ValueOf

func SetValue(i int)  {
	v := reflect.ValueOf(&i)
	v.Elem().SetInt(4)
	fmt.Println(i)
}

反射获取结构体信息

type Person struct {
	Id int `json:"id"`
	Name string `json:"name"`
	Age int
	Addr string
}

func main() {
	person := Person{Id:1,Name:"abc",Age:18,Addr:"xxx"}
	t := reflect.TypeOf(&person) // 注意必须为指针
	// 获取字段信息
	for i:=0;i<t.NumField();i++ {
		fmt.Println(t.Field(i))
	}

	// 获取tag信息
	field := t.Elem().Field(0)
	j := field.Tag.Get("json")
	fmt.Println(j)  // "id"

	// 获取方法信息
	for i:=0;i<t.NumMethod();i++ {
		fmt.Println(t.Method(i))
	}

	// 调用函数
	v := reflect.ValueOf(&person) // 注意必须为指针
	v2 := v.MethodByName("Get")
	v2.Call([]reflect.Value{})
}

垃圾回收

单个go文件查看gc

  1. 设置环境变量

     set GODEBUG=gctrace=1
    
  2. 在项目目录下通过cmd设置环境变量并启动go程序

  3. 术语

  • mark:标记阶段
  • markTermination:标记结束阶段
  • mutator assist:辅助GC,在GC过程中mutator线程会并发运行,而mutator assist机制会协助GC做一部分的工作
  • heaplive:在GO的内存管理中,span是内存页的基本单元,每页大小为8kb,同事GO会根据对象的大小不同而分配不同页数的span,而heaplive就代表者所有span的总大小
  • dedicated/fractional/idle,代表者不同的专注程度,其中dedicated模式最专注,是完整的GC回收行为,fractional只会干部分GC行为,idle最轻松
  • P:指处理器
  1. 含义
    gc 4 @0.254s 5%:2.0+1.0+9.9 ms clock, 16+0/2.0/2.0+79 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
  • gc 4:GC执行次数的编号,每次叠加,第4次GC
  • @0.254S:自程序启动后到当前的具体秒数
  • 5%:自程序启动以来在GC中花费的时间百分比
  • 2.0+1.0+9.9ms clock:GC的标记工作共使用的CPU时间占总CPU时间百分比
    - 2.0表示单个P在mark阶段的STW时间(stop the world//执行当前程序期间停止其他程序执行)
    - 1.0表示所有P的mark concurrent(并发标记)所使用的时间
    - 9.9表示单个P的markTermination阶段的STW时间
  • 16+0/2.0/2.0+79 ms cpu
    - 16:表示整个进程在mark阶段的STW停顿的时间
    - 0/2.0/2.0:0表示mutator assist占用的时间,2.0表示dedicated+fractional占用的时间
  • 4->4->0 MB
    - 4表示开始mark阶段当前的heap_live大小
    - 4表示开始markTermination阶段前的heap_live大小
    - 1表示被标记对象的大小
  • 5 MB goal:表示下一次触发GC回收的阈值是5MB
  • 8P:本次GC一共涉及多少个P

垃圾回收常用方法

  1. 引用计数
    原理是在没一个对象内部维护一个整数值,叫做这个对象的引用计数,当对象被引用时引用计数加一,当对象不被引用时引用计数减一,当引用计数为0时自动销毁对象
  • 回收的是内存空间
  • 链式引用都要跟着更新计数
  • 不能解决循环引用问题
  • 频繁更新引用计数降低了性能
  1. 标记-清除(golang 1.5以前用的)
    标记:从程序的根节点开始,递归遍历所有对象,将能遍历到的对象打上标记
    清除:将所有未标记的对象当作垃圾销毁
    缺点:在标记时必须暂停整个程序,递归遍历,会消耗很多时间

  2. 分代收集(java .Net使用)
    分代收集也是传统Mark-Sweep的一个改进
    原理:

  • 新对象放入0代
  • 当内存用量超过一个较小的阈值时,触发0代收集
  • 第0代幸存的对象(未被收集)放入1代
  • 只有当内存用量超过一个较高的阈值时,才会触发1代收集
  • 2代同理

三色标记算法(Go 1.5 1.6使用)

三色标记法是传统的Mark-Sweep的一个改进,是一个并发的GC算法
让系统的gc暂停时间能够预测
通过三个阶段确定要清除的对象有哪些
根节点对象:全局变量和函数栈里的对象
主要目的是减少STW时间
原理:

  • 首先创建三个集合:白、灰、黑
  • 将所有对象放入白色集合中
  • 然后从根节点开始遍历所有对象(注意这里并不递归遍历,只遍历一次),把遍历到的对象从白色集合放入灰色集合(灰色集合中都是根节点的对象)
  • 之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合(这里指灰色对象引用到的所有对象,包括灰色节点间接引用的那些对象,没有自节点的,也会被移动到黑色集合中)
  • 重复上面一步,直到灰色中无任何对象
  • 通过write-barrier(写屏障)检测对象有变化,重复以上操作,直到灰色中无任何对象
  • 收集所有白色对象(垃圾),还在白色集合中意味着已经找不到该对象在哪了,不可能会再被重新引用

一次垃圾回收完成后,将黑色集合变色成为白色集合,会进一步gc操作,依次循环

注意:如果没有STW,在标记的时候对象被引用了,会出现对象丢失现象,可以使用写屏障实现类似STW的效果,尽可能的缩短STW的时间

缺陷:垃圾产生的速度会大于垃圾收集的速度,这样会导致程序中的垃圾越来越多无法被收集掉

写屏障

原理:当某一个对象被标黑,此时这个对象又被其他对象引用,就把引用这个对象的对象变灰入队。比如a对象被标黑,a又被b引用,就把b标灰

强三色不变性:黑色对象不会指向白色对象,只会指向灰色对象或黑色对象

弱三色不变性:黑色对象指向白色对象必须包含一条从灰色对象经由多个白色对象的可达路径

  1. 插入写屏障(使满足强三色不变性)
  • 如果两个对象之间新建立引用,那么引用指向的对象就会被标记为灰色以满足强三色不变性
  • 标记其对应对象为灰色状态,这样就不存在黑色对象引用白色对象的情况了,满足强三色不变性
  1. 删除写屏障(使满足弱三色不变性)
  • 一个灰色对象指向一个白色对象的引用被删除,那么在删除之前写屏障检测到内存变化,就会把这个白色对象标灰
  1. 混合屏障
  • 插入写屏障和删除写屏障

gc调优

  1. 减少对象的分配,合理重复利用
  2. 避免string和[]byte转化
    string和[]byte转化的时候,底层数据结构会进行复制,导致gc效率变低
  3. 尽量少使用+拼接字符串
    针对他的每一个操作都会创建一个新的string,可以用strings.Join代替
  4. 尽量避免频繁创建对象{&abc{}、new(abc{})、make()}
  5. 程序开发阶段关注请求响应时间,每个功能优化一点时间,很多功能就很客观了

内存分配

内存管理流程

程序启动的时候,会先向操作系统申请一块虚拟内存,然后切小块后自己进行管理。申请到的内存快被分配了三个区域,在X64上分别是512MB(spans),16GB(bitmap),512GB(arena)大小

arena

堆区,用来动态分配内存,把内存分割成8KB大小的页,一些页组合起来称为mspan

bitmap

表示arena区域那些地址保存了对象,并且用4bit标志位表示对象是否包含指针、GC标记信息。bitmap中一个byte大小的内存对应arena区域中4个指针大小(指针大小为8B)的内存

spans

存放mspan的指针,也就是一些arena分割的页组合起来的内存管理基本单元,每个指针对应一页,创建mspan的时候,按页填充对应的spans区域,在回收对象时,根据地址很容易就能找到它所属的mspan

内存管理单元

mspan是Go中内存管理的基本单元,是由一片连续的8KB页组成的大块内存,mspan是一个包含起始地址、mspan规格、页的数量等内容的双端链表

type span struct {
	next *mspan // 链表后向指针,用于将spans连接起来
	prev *mspan // 链表前向指针,用于将spans连接起来
	startAddr uintptr // 起始地址,即所管理页的地址
	npages uintptr // 管理的页数
	nelems uintptr // 块个数,表示有多少块可以分配

	allocBits *gcBits // 分配位图,每一位代表一个块是否已分配
	allocCount uint16 // 已分配块的个数
	spanclass spanClass // class表中class ID,和Size Class相关
	
	elemsize uintptr // class表中的对象大小,即块大小
}

type span struct {
	start int
	end int
}

内存管理组件

  • mcache
    mcache在初始化时没有任何mspan资源,在使用过程中会动态从mcentral申请

  • mcentral
    为所有mcache提供切分好的mspan资源,每个mcentral对应一种sizeClass的mspan

  • mheap
    堆空间。当mcentral没有空闲msoan时,会向mheao申请

分配流程:
对象<=16B的对象使用mcache的tiny分配器分配
16B对象<=32KB,首先计算对象的规格大小(sizeClass,共76种),然后使用mcache中相应规格大小的mspan分配
对象>32B,直接从mheap上分配
mcache->mcentral->mheap
如果mcache没有相应规格大小的mspan,则向mcentral申请
如果mcentral没有相应规格大小的mspan,则向mheap申请
如果mheap没有相应规格大小的mspan,则向操作系统申请

TCMalloc算法

Google为C语言开发的一种内存管理算法

把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。

优势:

  • 比glibc 2.3 malloc更快,glibc有个ptmalloc2独立库,完成一次malloc需要300ns左右,tcmalloc只需要50ns
  • 小对象没有锁的操作,没有锁冲突;大对象使用更加高效的自旋锁循环请求,减少锁冲突
  • 使用线程本地缓存来存储预分配的内存对象,小的对象可以直接分配到线程本地缓存(<=32KB为小对象)

回收过程

  • 当线程缓存中所有空闲对象的大小超过2MB的时候,垃圾回收期会自动进行回收,线程数增加时候,垃圾回收的阈值会减少以避免内存浪费
  • 遍历缓存中的所有空闲链表,从中移动L/2个对象到对应的中央链表中。L记录了自从上一次垃圾收集操作后本链的最小长度。如果一个线程停止使用某个特定大小的对象,改大小的所有对象会很快的从线程缓存中迁移到中央空闲链中,以便被其他线程来使用

常见模型

CSP并发模型

两个独立的并发实体进行通信的一种设计思路

GPM调度模型

  • G:表示goroutine,G并非执行体,每个G要绑定到P才能被调度执行
  • P:表示逻辑处理器,G和M的调度对象
    • 对G来说,P相当于CPU核,G只有绑定到P才能被调度
    • 对M来说,P提供了相关的执行环境,如内存分配状态,任务队列,P的数量决定了系统内最大可并行的G的数量
  • M:对内核级线程的封装,数量对应真实的CPU数(真正干活的对象)
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 ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值