当前位置: 首页 > news >正文

Go语言系统学习笔记(三):杂项篇

1. 写在前面

公司的新业务开发需要用到go语言,虽然之前没接触过这门语言,但在大模型的帮助下,边看项目边写代码也能进行go的项目开发,不过,写了一段时间代码之后,总感觉对go语言本身,我的知识体系里面并没有一个比较完整的架子,学习到的知识零零散散,不成体系,虽然能完成工作,但心里比较虚,没有沉淀下知识。所以想借着这个机会,用两周的时间系统的学习下go语言, 在知识体系里面搭一个属于go语言的知识框架,把知识拎起来, 也便于后续知识的扩充与回看。

此次学习,依然是业余时间看文档的方式搭建知识框架(工作之后发现看视频比较慢,没时间看), 参看的文档是C语言中文网go教程, 上面内容整理的很详细,非常适合初学者搭建知识体系。只不过内容比较多, 这次还是和之前一样, 整体过一遍教程, 把我觉得现阶段比较关键的知识梳理出来,过于简单的知识作整合,对重点知识,用其他一些资料补充,再用一些实验作为辅助理解,先跳过一些demo示例实践, 基础知识作整理,关键知识作扩展搭建基础框架,后面再从项目中提炼新知识作补充完善框架

PS: 由于我有C/C++、Java、Python等语言基础,所以针对教程的前面常识部分作了整合和删减,小白的话建议去原网站学习哈。

Go语言系统部分打算用3篇文章搭建知识框架,基础篇、进阶篇和杂项篇,每一篇里面的内容各个模块划分的比较清晰,这样后面针对新知识方便补充。今天是最后一篇内容, 想整理教程里面额外的一些杂项,主要包括常用的包以及文件处理部分,由于这两块内容偏实践, 我没有整理太多,还是先有个简单的架子,后面接触到之后随时补充。

大纲如下:

  • go包
  • go文件处理

Ok, let’s go!

2 包

2.1 初识

Go语言是使用包来组织源代码的,包(package)是多个 Go 源码的集合,是一种高级的代码复用方案。Go语言中为我们提供了很多内置包,如 fmt、os、io 等。

任何源代码文件必须属于某个包,同时源码文件的第一行有效代码必须是package pacakgeName 语句,通过该语句声明自己所在的包

// 导入方式
import "包的路径"
import ("包 1 的路径""包 2 的路径"
)// 导入路径
// 包的绝对路径就是GOROOT/src/或GOPATH/src/后面包的存放路径 , 最好是用这种方式导入, 不要用相对路径,容易出错
import "database/sql/driver"
import "database/sql"// 引用格式
// 最常规
import "fmt"
fmt.Println("hello world")// 自定义别名 类似Python的 improt xx as x
import F "fmt"
F.Println("hello world")// 省略引用: 相当于把 fmt 包直接合并到当前程序中,在使用 fmt 包内的方法可以直接引用
import . "fmt"
Println("hello world")// 匿名引用  只是希望执行包初始化的 init 函数,而不使用包内部的数据
import _ "fmt"
// 后面不用这个包里面的方法, 编译器不会报错// init()调用顺序为 main() 中引用的包,以深度优先顺序初始化  main→A→B→C 则 init的调用方法: C.init→B.init→A.init→main
// 同一个包中的多个 init() 函数的调用顺序不可预期

我们可以自定义的包,方便管理自己写的源代码,但有几点需要注意:

  • 创建的自定义的包需要将其放在 GOPATH 的 src 目录下(也可以是 src 目录下的某个子目录),而且两个不同的包不能放在同一目录下,这样会引起编译错误
  • 一个包中可以有任意多个文件,文件的名字也没有任何规定(但后缀必须是 .go),这里我们假设包名就是 .go 的文件名(如果一个包有多个 .go 文件,则其中会有一个 .go 文件的文件名和包名相同)。
  • 使用 import 语句导入包时,使用的是包所属文件夹的名称
  • 包中的函数名第一个字母要大写,否则无法在外部调用,同样的,如果想让包内的变量,结构体,接口,类型,常量等在包外被访问,也需要字段名或者方法名的首字母要大写
  • 调用自定义包时使用 包名.函数名 的方式

2.2 go语言封装

在Go语言中封装就是把抽象出来的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只能通过被授权的方法,才能对字段进行操作。

封装的好处:1. 隐藏实现细节;2. 可以对数据据进行验证,保证数据安全合理。

如何体现封装:

  • 对结构体中的属性进行封装;
  • 通过方法,包,实现封装。

封装的实现步骤:

  • 将结构体、字段的首字母小写;
  • 给结构体所在的包提供一个工厂模式的函数,首字母大写,类似一个构造函数;
  • 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值;
  • 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值。
// 对于员工,不能随便查看年龄,工资等隐私,并对输入的年龄进行合理的验证
目录结构 
* main* main.go
* model* model.go// model源码
package model
import "fmt"
type person struct {Name stringage int   //其它包不能直接访问..sal float64
}
//写一个工厂模式的函数,相当于构造函数
func NewPerson(name string) *person {return &person{Name : name,}
}
//为了访问age 和 sal 我们编写一对SetXxx的方法和GetXxx的方法
func (p *person) SetAge(age int) {if age >0 && age <150 {p.age = age} else {fmt.Println("年龄范围不正确..")//给程序员给一个默认值}
}
func (p *person) GetAge() int {return p.age
}
func (p *person) SetSal(sal float64) {if sal >= 3000 && sal <= 30000 {p.sal = sal} else {fmt.Println("薪水范围不正确..")}
}
func (p *person) GetSal() float64 {return p.sal
}// main里面
package main
import ("fmt""../model"    // 这里就是相对路径导入方式
)
func main() {p := model.NewPerson("smith")p.SetAge(18)p.SetSal(5000)fmt.Println(p)fmt.Println(p.Name, " age =", p.GetAge(), " sal = ", p.GetSal())

2.3 go语言内置包

安装go的时候自动会安装一些内置包, 在$GOROOT/src/pkg 目录中可以查看这些包,常用的整理如下:

包名作用
fmt格式化的标准输入输出,这与C语言中的 printf 和 scanf 类似。其中的 fmt.Printf() 和 fmt.Println() 是开发者使用最为频繁的函数
sort对切片和用户定义的集合排序
strconv将字符串转换成基本数据类型,或者从基本数据类型转换为字符串的功能
os操作系统函数接口
sync实现多线程中锁机制以及其他同步互斥机制
flag命令行参数的规则定义和传入参数解析的功能, 很常用。
encoding/json提供了对 JSON 的基本支持,比如从一个对象序列化为 JSON 字符串,或者从 JSON 字符串反序列化出一个具体的对象等
net/httpHTTP 相关服务,主要包括 http 请求、响应和 URL 的解析,以及基本的 http 客户端和扩展的 http 服务
reflect运行时反射,允许程序通过抽象类型操作对象。通常用于处理静态类型 interface{} 的值,并且通过 Typeof 解析出其动态类型信息,通常会返回一个有接口类型 Type 的对象
os/exec执行linux命令
strings处理字符串的一些函数集合,包括合并、查找、分割、比较、后缀检查、索引、大小写处理等等
log在程序中输出日志

2.4 单例模式

单例模式是常用的模式之一,在它的核心结构中只包含一个被称为单例的特殊类,能够保证系统运行中一个类只创建一个实例

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

应用实例:

  • 1、一个班级只有一个班主任。
  • 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
  • 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

优点:

  • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景:

  • 1、要求生产唯一序列号。
  • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

实现方式(四种):

// 懒汉式  创建对象时比较懒,先不急着创建对象,在需要加载配置文件的时候再去创建
// 非线程安全  多线程情况下可能会创建多次对象
//使用结构体代替类
type Tool struct {values int
}
//建立私有变量
var instance *Tool
//获取单例对象的方法,引用传递返回
func GetInstance() *Tool {if instance == nil {instance = new(Tool)}return instance
}
// 为了保证线程安全, 采用加锁的方式, 但性能会有所下降
//锁对象
var lock sync.Mutex
//加锁保证线程安全
func GetInstance() *Tool {lock.Lock()      // 加锁defer lock.Unlock()   // 延迟释放, 等函数执行完释放锁if instance == nil {instance = new(Tool)}return instance
}// 饿汉式 在系统初始化的时候就已经把对象创建好了,需要用的时候直接拿过来用就好了
// 直接创建好对象,不需要判断为空,同时也是线程安全,唯一的缺点是在导入包的同时会创建该对象,并持续占有在内存中
type cfg struct {
}
var cfg *config
func init()  {cfg = new(config)
}
// NewConfig 提供获取实例的方法
func NewConfig() *config {return cfg
}
type config struct {  
}
//全局变量
var cfg *config = new(config)
// NewConfig 提供获取实例的方法
func NewConfig() *config {return cfg
}// 双重检查 
// 懒汉式(线程安全)的基础上再进行优化,减少加锁的操作,保证线程安全的同时不影响性能
//锁对象
var lock sync.Mutex
//第一次判断不加锁,第二次加锁保证线程安全,一旦对象建立后,获取对象就不用加锁了。
func GetInstance() *Tool {if instance == nil {lock.Lock()if instance == nil {instance = new(Tool)}lock.Unlock()}return instance
}// Sync.Once  通过 sync.Once 来确保创建对象的方法只执行一次, 内部本质上也是双重检查的方式
var once sync.Once
func GetInstance() *Tool {once.Do(func() {instance = new(Tool)})return instance
}

2.5 sync包与锁

sync 包里提供了互斥锁 Mutex 和读写锁 RWMutex 用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况。

锁是 sync 包中的核心,它主要有两个方法,分别是加锁(Lock)和解锁(Unlock)。

在并发的情况下,多个线程或协程同时其修改一个变量,使用锁能保证在某一时间内,只有一个协程或线程修改这一变量

// 不使用锁的并发情况,可能得不到想要的结果
package main
import ("fmt""time"
)
func main() {var a = 0for i := 0; i < 1000; i++ {go func(idx int) {a += 1fmt.Println(a)}(i)}time.Sleep(time.Second)
}
// 结果
831
833
835
837
839
841
843
845
847
682
851
853
855
857
858
860
862
686
752// 协程的执行顺序: 1. 从寄存器读取 a 的值;  2. 然后做加法运算;  3. 最后写到寄存器。
// 按照上面的顺序,假如有一个协程取得 a 的值为 3,然后执行加法运算,此时又有一个协程对 a 进行取值,得到的值同样是 3,最终两个协程的返回结果是相同的。
// 而锁的概念就是,当一个协程正在处理 a 时将 a 锁定,其它协程需要等待该协程处理完成并将 a 解锁后才能再进行操作,也就是说同时处理 a 的协程只能有一个,从而避免上面示例中的情况出现

互斥锁: 解决上面的问题,可以用互斥锁搞定, 互斥锁这个名字来自互斥的概念。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界代码。

func (m *Mutex) Lock()
func (m *Mutex) Unlock()package main
import ("fmt""time""sync"
)
func main() {var a = 0var lock sync.Mutexfor i := 0; i < 1000; i++ {go func(idx int) {lock.Lock()    // 加锁defer lock.Unlock()   // 函数执行完了之后释放锁a += 1fmt.Printf("goroutine %d, a=%d\n", idx, a)}(i)}time.Sleep(time.Second)
}// 一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定)

读写锁有如下四个方法:

  • 写操作的锁定和解锁分别是func (*RWMutex) Lockfunc (*RWMutex) Unlock
  • 读操作的锁定和解锁分别是func (*RWMutex) Rlockfunc (*RWMutex) RUnlock

读写锁的区别在于:

  • 当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;
  • 当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;
  • 当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。

所以说这里的读锁定(RLock)目的其实是告诉写锁定,有很多协程或者进程正在读取数据,写操作需要等它们读(读解锁)完才能进行写(写锁定)。即

  • 同时只能有一个 goroutine 能够获得写锁定;
  • 同时可以有任意多个 gorouinte 获得读锁定;
  • 同时只能存在写锁定或读锁定(读和写互斥)

理解起来就是:

  • 如果有人在写的时候, 其他人读和写都不行, 因为其他人如果读, 可能在写的前后读的数据不一致,产生幻读,其他人如果写,那造成写冲突
  • 有人在读的时候,其他人可以读,但是不能写,防止别人读错, 因为写可能会导致正在读的人读错
package main
import ("fmt""math/rand""sync"
)
// count的全局变量用于读写操作
var count int
// 读写锁实例
var rw sync.RWMutexfunc main() {// 搞一个通道,容量为10ch := make(chan struct{}, 10)// 启动了5个goroutine用于读for i := 0; i < 5; i++ {go read(i, ch)}// 启动了5个goroutine用于写for i := 0; i < 5; i++ {go write(i, ch)}// 通过从ch通道读取10次数据(空结构体struct{}),确保之前启动的所有goroutine完成它们的操作。// 这是一种等待所有goroutine完成的简单方法for i := 0; i < 10; i++ {<-ch}
}
func read(n int, ch chan struct{}) {rw.RLock()   // 加读锁fmt.Printf("goroutine %d 进入读操作...\n", n)v := count     // 读数据countfmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)rw.RUnlock()   // 释放读锁ch <- struct{}{}   // 向ch通道发送一个空结构体,表示读操作完成
}
func write(n int, ch chan struct{}) {rw.Lock()        // 加写锁fmt.Printf("goroutine %d 进入写操作...\n", n)v := rand.Intn(1000)     // 修改countcount = vfmt.Printf("goroutine %d 写入结束,新值为:%d\n", n, v)rw.Unlock()         // 释放写锁ch <- struct{}{}   // 向ch发送一个空结构体,表明写操作完成
}// result
goroutine 0 进入读操作...
goroutine 0 读取结束,值为:0
goroutine 4 进入写操作...
goroutine 4 写入结束,新值为:81
goroutine 1 进入读操作...
goroutine 1 读取结束,值为:81
goroutine 4 进入读操作...
goroutine 4 读取结束,值为:81
goroutine 2 进入读操作...
goroutine 2 读取结束,值为:81
goroutine 3 进入读操作...
goroutine 3 读取结束,值为:81
goroutine 0 进入写操作...
goroutine 0 写入结束,新值为:887
goroutine 1 进入写操作...
goroutine 1 写入结束,新值为:847
goroutine 2 进入写操作...
goroutine 2 写入结束,新值为:59
goroutine 3 进入写操作...
goroutine 3 写入结束,新值为:81

上面这段代码展示了如何在Go中使用读写锁和goroutines安全地执行并发读写操作,并通过一个通道来同步goroutine的执行,确保所有goroutine执行完成后主函数才退出。读写锁sync.RWMutex允许多个goroutines同时读取一个共享资源而不会相互干扰,但是如果有goroutine正在写入,其他goroutine无论是读取还是写入操作都会被阻塞,直到写锁被释放。

下面看一个读读不互斥的例子和写读互斥的例子:

// 两个哥们同时读
package main
import ("sync""time"
)
var m *sync.RWMutex
func main() {m = new(sync.RWMutex)// 多个同时读, 互不影响go read(1)go read(2)time.Sleep(2*time.Second)
}
func read(i int) {println(i,"read start")m.RLock()println(i,"reading")time.Sleep(1*time.Second)m.RUnlock()println(i,"read over")
}// 结果
1 read start
1 reading          // 1读的时候,2也可以读
2 read start
2 reading
1 read over
2 read overfunc main() {m = new(sync.RWMutex)// 写的时候啥也不能干go write(1)go read(2)go write(3)time.Sleep(5*time.Second)   // 这个如果sleep时间太短,其他协程可能执行不完
}
func read(i int) {println(i,"read start")m.RLock()println(i,"reading")time.Sleep(1*time.Second)m.RUnlock()println(i,"read over")
}
func write(i int) {println(i,"write start")m.Lock()println(i,"writing")time.Sleep(1*time.Second)m.Unlock()println(i,"write over")
}// 结果
1 write start
1 writing
2 read start
3 write start
1 write over
2 reading
2 read over
3 writing
3 write over

write(1) 是第一个尝试获取写锁的goroutine。read(2)会被阻塞,直到写锁被释放,但因为所有goroutine都被设计要运行超过主函数的time.Sleep(2*time.Second)限制,read(2)可能来不及在主函数结束前完成。write(3)的情况和read(2)类似,它会等待写锁被write(1)释放。

为了防止这种情况,可以考虑根据实际需要调整goroutine的启动逻辑,以及增大main()函数中time.Sleep的持续时间,以确保所有goroutine都有足够的时间执行。

Go语言并发模型的一个关键特征是通过goroutines和channels来实现并发编程,而在并发编程中,死锁、活锁和饥饿是三种常见的问题

// 死锁是指两个或两个以上的执行单元(在Go中通常是goroutines)互相等待对方释放资源,导致它们永久阻塞的一种情况。
// 如果每一个goroutine在等待另一个goroutine同时,这就形成了一个循环等待的条件,从而造成死锁。
func main() {ch1 := make(chan int)ch2 := make(chan int)    go func() {<-ch1ch2 <- 1}()go func() {<-ch2ch1 <- 1}()// 这会导致死锁,因为两个goroutine都在等待对方发送数据
}// 活锁是指程序没有被阻塞(即执行单元都在运行),但是由于某种条件始终不能得到满足,导致程序无法向前推进。
// 虽然线程或goroutines还在继续执行,但是它们却做不了任何有用的工作。
// 场景: 两个goroutine彼此都在尝试避让对方(例如,基于某些条件重试操作,但这些条件却令彼此永远无法继续)
func main() {ch := make(chan int)done := make(chan bool)var sharedResource intgo func() { // Goroutine 1试图增加sharedResource到一个特定的值for sharedResource < 10 {select {case val := <-ch:sharedResource += valdefault:fmt.Println("Goroutine 1等待增加")time.Sleep(100 * time.Millisecond) // 等待资源,希望稍后能增加它}}done <- true}()go func() { // Goroutine 2试图减少sharedResource到一个特定的值for sharedResource > -10 {select {case val := <-ch:sharedResource -= valdefault:fmt.Println("Goroutine 2等待减少")time.Sleep(100 * time.Millisecond) // 等待资源,希望稍后能减少它}}done <- true}()go func() { // Goroutine 3周期性地尝试发送变化到资源for {select {case ch <- 1: // 尝试发送1到channelfmt.Println("发送1")case ch <- 2: // 尝试发送2到channelfmt.Println("发送2")case <-time.After(50 * time.Millisecond): // 每50毫秒发送一次资源// Nothing to do here.}time.Sleep(50 * time.Millisecond) // 等待50毫秒再试}}()// 等待至少一个goroutine完成<-done
}
// 有三个goroutine:第一个goroutine试图连续增加一个共享资源的值到10,第二个goroutine试图减少它到-10,而第三个周期性地尝试改变资源。前两个goroutine在默认情况下只是等待,并且定期打印出它们在等待的消息。第三个goroutine在发送值后等待一段时间再尝试发送。由于这三个goroutine之间的交互方式,它们可能永远不会让 sharedResource 达到任何一个goroutine的完成条件,因为另一方马上就会扭转它们的操作。虽然这些goroutines在做“工作”,但是这种工作并不会推动程序朝着完成目标前进,因此就发生了活锁// 饥饿发生在一个或多个执行单元无法获得它们所需要的资源,因而无法进行下去。
// 这通常是由于资源被其他执行单元长时间占用所致。在饥饿的场景中,受影响的执行单元并没有被死锁(因为没有循环等待的条件),而是因为总是有其他执行单元比它更优先获得资源。
func main() {runtime.GOMAXPROCS(3)var wg sync.WaitGroupconst runtime = 1 * time.Secondvar sharedLock sync.MutexgreedyWorker := func() {defer wg.Done()var count intfor begin := time.Now(); time.Since(begin) <= runtime; {sharedLock.Lock()time.Sleep(3 * time.Nanosecond)sharedLock.Unlock()count++}fmt.Printf("Greedy worker was able to execute %v work loops\n", count)}politeWorker := func() {defer wg.Done()var count intfor begin := time.Now(); time.Since(begin) <= runtime; {sharedLock.Lock()time.Sleep(1 * time.Nanosecond)sharedLock.Unlock()sharedLock.Lock()time.Sleep(1 * time.Nanosecond)sharedLock.Unlock()sharedLock.Lock()time.Sleep(1 * time.Nanosecond)sharedLock.Unlock()count++}fmt.Printf("Polite worker was able to execute %v work loops\n", count)}wg.Add(2)go greedyWorker()go politeWorker()wg.Wait()
}
// Greedy worker was able to execute 276 work loops
// Polite worker was able to execute 92 work loops// 贪婪的 worker 会贪婪地抢占共享锁,以完成整个工作循环,而平和的 worker 则试图只在需要时锁定。
// 两种 worker 都做同样多的模拟工作(sleeping 时间为 3ns),可以看到,在同样的时间里,贪婪的 worker 工作量几乎是平和的 worker 工作量的两倍!

解决方法:

  • 死锁:确保程序设计避免循环等待,或者使用超时时间来避免无限等待。
  • 活锁:添加随机因素或者调整重试策略,使得重试操作最终能够成功。
  • 饥饿:公平地分配资源,或者使用更复杂的锁机制来确保长时间等待的执行单元也能获得资源。

2.6 Inject包与依赖注入

正常情况下,对函数或方法的调用是主动直接行为,在调用某个函数之前需要清楚地知道被调函数的名称是什么,参数有哪些类型等等。

所谓的控制反转就是将这种主动行为变成间接的行为,我们不用直接调用函数或对象,而是借助框架代码进行间接的调用和初始化,这种行为称作“控制反转”,库和框架能很好的解释控制反转的概念。

依赖注入是实现控制反转的一种方法,如果说控制反转是一种设计思想,那么依赖注入就是这种思想的一种实现,通过注入参数或实例的方式实现控制反转。如果没有特殊说明,我们可以认为依赖注入和控制反转是一个东西。

控制反转的价值在于解耦,有了控制反转就不需要将代码写死,可以让控制反转的的框架代码读取配置,动态的构建对象, 这一点在Java的Spring框架中突出。

inject 是依赖注入的Go语言实现,它能在运行时注入参数,调用方法,是 Martini 框架(Go语言中著名的 Web 框架)的基础核心。

// Inject包借助反射实现函数的注入调用
import ("fmt""github.com/codegangsta/inject"
)
type S1 interface{}
type S2 interface{}
func Format(name string, company S1, level S2, age int) {fmt.Printf("name = %s, company=%s, level=%s, age = %d!\n", name, company, level, age)
}
func main() {//控制实例的创建inj := inject.New()//实参注入inj.Map("tom")inj.MapTo("tencent", (*S1)(nil))inj.MapTo("T4", (*S2)(nil))inj.Map(23)//函数反转调用inj.Invoke(Format)   // name = tom, company=tencent, level=T4, age = 23!
}
//  inject 提供了一种注入参数调用函数的通用功能,inject.New() 相当于创建了一个控制实例,由其来实现对函数的注入调用// inject 包对 struct 类型的注入
type S1 interface{}
type S2 interface{}
type Staff struct {Name    string `inject`Company S1     `inject`Level   S2     `inject`Age     int    `inject`
}
func main() {//创建被注入实例s := Staff{}//控制实例的创建inj := inject.New()//初始化注入值inj.Map("tom")inj.MapTo("tencent", (*S1)(nil))inj.MapTo("T4", (*S2)(nil))inj.Map(23)//实现对 struct 注入inj.Apply(&s)//打印结果fmt.Printf("s = %v\n", s)  // s = {tom tencent T4 23}
}

其他包,例如time、os、flag包等, 等用到的时候再补充。

3 文件处理

这里主要是整理用go如何去读各种类型的文件以及写各种类型的文件。

3.1 Json文件读写

package main
import ("encoding/json""fmt""os"
)
type People struct {Name   stringAge    int
}
func main() {peoples := []People{{"zhongqiang", 20}, {"zhangsan",30}}// 创建文件filePtr, err := os.Create("info.json")if err != nil {fmt.Println("文件创建失败", err.Error())return}defer filePtr.Close()// 创建Json编码器encoder := json.NewEncoder(filePtr)err = encoder.Encode(peoples)if err != nil {fmt.Println("编码错误", err.Error())} else {fmt.Println("编码成功")}// 读json文件filePtr1, err1 := os.Open("info.json")if err1 != nil {fmt.Println("文件打开失败 [Err:%s]", err1.Error())return}defer filePtr1.Close()var peoples1 []People// 创建json解码器decoder := json.NewDecoder(filePtr1)err = decoder.Decode(&peoples1)if err != nil {fmt.Println("解码失败", err.Error())} else {fmt.Println("解码成功")fmt.Println(peoples1)  //  [{zhongqiang 20} {zhangsan 30}]}
}

3.2 XML文件的读写

package main
import ("encoding/xml""fmt""os"
)
type People struct {Name   stringAge    int
}
func main() {peoples := []People{{"zhongqiang", 20}, {"zhangsan",30}}// 创建文件filePtr, err := os.Create("info.xml")if err != nil {fmt.Println("文件创建失败", err.Error())return}defer filePtr.Close()// 创建xml编码器encoder := xml.NewEncoder(filePtr)err = encoder.Encode(peoples)if err != nil {fmt.Println("编码错误", err.Error())} else {fmt.Println("编码成功")}// 读xml文件filePtr1, err1 := os.Open("info.xml")if err1 != nil {fmt.Println("文件打开失败 [Err:%s]", err1.Error())return}defer filePtr1.Close()peoples1 := People{}// 创建xml解码器decoder := xml.NewDecoder(filePtr1)err = decoder.Decode(&peoples1)if err != nil {fmt.Println("解码失败", err.Error())} else {fmt.Println("解码成功")fmt.Println(peoples1)  //  [{zhongqiang 20} {zhangsan 30}]}
}

3.3 Gob读取

这个是go语言专属, 类似于python里面的Pickle,以二进制形式序列化和反序列化数据

func main() {// 写info := map[string]string{"name": "zhongqiang","age": "30",}name := "demo.gob"File, _ := os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0777)defer File.Close()enc := gob.NewEncoder(File)if err := enc.Encode(info); err != nil {fmt.Println(err)}// 读var M map[string]stringFile1, _ := os.Open("demo.gob")D := gob.NewDecoder(File1)D.Decode(&M)fmt.Println(M)  // map[age:30 name:zhongqiang]
}

3.4 txt文件读取

package main
import ("bufio""fmt""os""io"
)
func main() {//创建一个新文件,写入内容filePath := "./output.txt"file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)if err != nil {fmt.Printf("打开文件错误= %v \n", err)return}//及时关闭defer file.Close()//写入内容str1 := "zhongqiang\n" // \n\r表示换行  txt文件要看到换行效果要用 \r\nstr2 := "zhangsan\n"str3 := "lisi\n"//写入时,使用带缓存的 *Writerwriter := bufio.NewWriter(file)writer.WriteString(str1)writer.WriteString(str2)writer.WriteString(str3)//因为 writer 是带缓存的,因此在调用 WriterString 方法时,内容是先写入缓存的//所以要调用 flush方法,将缓存的数据真正写入到文件中。writer.Flush()//打开文件file1, err1 := os.Open("./output.txt")if err1 != nil {fmt.Println("文件打开失败 = ", err1)}//及时关闭 file 句柄,否则会有内存泄漏defer file1.Close()//创建一个 *Reader , 是带缓冲的reader := bufio.NewReader(file1)for {str, err := reader.ReadString('\n') //读到一个换行就结束if err == io.EOF {                  //io.EOF 表示文件的末尾break}fmt.Print(str)}fmt.Println("文件读取结束...")
}// func OpenFile(name string, flag int, perm FileMode) (file *File, err error)
// 下面列举了一些常用的 flag 文件处理参数:// O_RDONLY:只读模式打开文件;// O_WRONLY:只写模式打开文件;// O_RDWR:读写模式打开文件;// O_APPEND:写操作时将数据附加到文件尾部(追加);// O_CREATE:如果不存在将创建一个新文件;// O_EXCL:和 O_CREATE 配合使用,文件必须不存在,否则返回一个错误;// O_SYNC:当进行一系列写操作时,每次都要等待上次的 I/O 操作完成再进行;// O_TRUNC:如果可能,在打开时清空文件。

先整理这几个常用的,后面如果再遇到新的,现补充。

4. 小总

这篇文章就整理到这里吧, 内容比较简单,也无需记住, 偏实践, 主要还是当作文档随时查询的时候方便。 到这里整个go语言学习系列就结束了, 学习时长2周, 把go的整体内容过了一遍, 3篇笔记搭建了一个知识框架,后面如果再进行go的开发,碰到新知识,就大概知道是哪块的内容,知道和哪些内容有关联了,然后再补充到对应的体系里面,慢慢完善就好。

我还是比较喜欢这种体系框架式的学习方式,这样能让碎片化的知识有关联, 更容易理解和记录,学习知识,不能孤立的学习某个点,得想办法联系到已有的知识框架中,然后横向和纵向反复和其他点作对比,才能更好的消化它, 如果没有合适的知识框架放,就得需要突击一段时间学习,整理一个初版的框架出来,这个突击不要求能把知识学习多么深入,只是为了先摸一遍,看看到底有啥,先有个大概,然后在实践里面去完善架子,慢慢的就有新的体系。 这是我理想里面的知识“大同”, 很多领域,很多知识,其实底层是相通的, 是有关联的,还得继续多读书,多学习,多扩充知识的广度与深度,慢慢的就会有感觉, 加油呀 😉

相关文章:

Go语言系统学习笔记(三):杂项篇

1. 写在前面 公司的新业务开发需要用到go语言&#xff0c;虽然之前没接触过这门语言&#xff0c;但在大模型的帮助下&#xff0c;边看项目边写代码也能进行go的项目开发&#xff0c;不过&#xff0c;写了一段时间代码之后&#xff0c;总感觉对go语言本身&#xff0c;我的知识体…...

黄仁勋炉边对话:创业的超能力与英伟达的加速计算之旅

在TiECon 2024大会上&#xff0c;英伟达的创始人兼CEO黄仁勋与风投公司Mayfield的管理合伙人纳文查德哈进行了一场深入的炉边对话。黄仁勋不仅分享了英伟达的创业故事&#xff0c;还谈到了他对创业和加速计算的深刻见解。下面是我对这次对话的总结&#xff0c;希望能给正在创业…...

.NET开源、功能强大、跨平台的图表库LiveChart2

LiveCharts2 是 从LiveCharts演变而来,它修复了其前身的主要设计问题,它专注于在任何地方运行,提高了灵活性,并继承LiveCharts原有功能。 极其灵活的数据展示图库 (效果图) 开始使用 Live charts 是 .Net 的跨平台图表库,请访问 https://livecharts.dev 并查看目标平…...

疯狂学英语

我上本科的时候&#xff0c;学校出国留学的气氛不浓厚&#xff0c;我们班只有一名同学有出国留学的倾向&#xff0c;我们宿舍八个人没有一个考虑过留学。 只有小昊&#xff0c;在本校上了研究生之后&#xff0c;不知道受到什么影响&#xff0c;想出国留学。那时候小昊利用一切…...

LeetCode //C - 93. Restore IP Addresses

93. Restore IP Addresses A valid IP address consists of exactly four integers separated by single dots. Each integer is between 0 and 255 (inclusive) and cannot have leading zeros. For example, “0.1.2.201” and “192.168.1.1” are valid IP addresses, bu…...

【数据结构】栈和队列OJ面试题

20. 有效的括号 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a;由于C语言没有栈的接口&#xff0c;所以我们需要自己造一个“模子”。我们直接copy之前的实现的栈的接口就可以了&#xff08;可以看我之前的博客【数据结构】栈和队列-CSDN博客copy接口&#xff09;&…...

【联邦学习——手动搭建简易联邦学习】

1. 目的 用于记录自己在手写联邦学习相关实验时碰到的一些问题&#xff0c;方便自己进行回顾。 2. 代码 2.1 本地模型计算梯度更新 # 比较训练前后的参数变化 def compare_weights(new_model, old_model):weight_updates {}for layer_name, params in new_model.state_dic…...

Springboot项目如何创建单元测试

文章目录 目录 文章目录 前言 一、SpringBoot单元测试的使用 1.1 引入依赖 1.2 创建单元测试类 二、Spring Boot使用Mockito进行单元测试 2.1 Mockito中经常使用的注解以及注解的作用 2.2 使用Mockito测试类中的方法 2.3 使用Mockito测试Controller层的方法 2.4 mock…...

Win10 如何同时保留两个CUDA版本并自由切换使用

环境&#xff1a; Win10 专业版 CUDA11.3 CUDA11.8 问题描述&#xff1a; Win10 如何同时保留两个CUDA版本并自由切换 解决方案&#xff1a; 在同一台计算机上安装两个CUDA版本并进行切换可以通过一些环境配置来实现。这通常涉及到管理环境变量&#xff0c;特别是PATH和L…...

实验室纳新宣讲会(java后端)

前言 这是陈旧已久的草稿2021-09-16 15:41:38 当时我进入实验室&#xff0c;也是大二了&#xff0c;实验室纳新需要宣讲&#xff0c; 但是当时有疫情&#xff0c;又没宣讲成。 现在2024-5-12 22:00:39&#xff0c;发布到[个人]专栏中。 实验室纳新宣讲会&#xff08;java后…...

class常量池、运行时常量池和字符串常量池的关系

类常量池、运行时常量池和字符串常量池这三种常量池&#xff0c;在Java中扮演着不同但又相互关联的角色。理解它们之间的关系&#xff0c;有助于深入理解Java虚拟机&#xff08;JVM&#xff09;的内部工作机制&#xff0c;尤其是在类加载、内存分配和字符串处理方面。 类常量池…...

Java | Leetcode Java题解之第88题合并两个有序数组

题目&#xff1a; 题解&#xff1a; class Solution {public void merge(int[] nums1, int m, int[] nums2, int n) {int p1 m - 1, p2 n - 1;int tail m n - 1;int cur;while (p1 > 0 || p2 > 0) {if (p1 -1) {cur nums2[p2--];} else if (p2 -1) {cur nums1[p…...

韵搜坊(全栈)-- 前后端初始化

文章目录 前端初始化后端初始化 前端初始化 使用ant design of vue 组件库 官网快速上手&#xff1a;https://www.antdv.com/docs/vue/getting-started-cn 安装脚手架工具 进入cmd $ npm install -g vue/cli # OR $ yarn global add vue/cli创建一个项目 $ vue create ant…...

Android:资源的管理,Glide图片加载框架的使用

目录 一&#xff0c;Android资源分类 1.使用res目录下的资源 res目录下资源的使用&#xff1a; 2.使用assets目录下的资源 assets目录下的资源的使用&#xff1a; 二&#xff0c;glide图片加载框架 1.glide简介 2.下载和设置 3.基本用法 4.占位符&#xff08;Placehold…...

conll-2012-formatted-ontonotes-5.0中文数据格式说明

CoNLL-2012 数据格式是用于自然语言处理任务的一种常见格式&#xff0c;特别是在命名实体识别、词性标注、句法分析和语义角色标注等领域。这种格式在 CoNLL-2012 共享任务中被广泛使用&#xff0c;该任务主要集中在语义角色标注上。 CoNLL-2012 数据格式通常包括多列&#xf…...

SpringBoot集成Seata分布式事务OpenFeign远程调用

Docker Desktop 安装Seata Server seata 本质上是一个服务&#xff0c;用docker安装更方便&#xff0c;配置默认&#xff1a;file docker run -d --name seata-server -p 8091:8091 -p 7091:7091 seataio/seata-server:2.0.0与SpringBoot集成 表结构 项目目录 dynamic和dyna…...

视觉检测系统,是否所有产品都可以进行视觉检测?

视觉检测系统作为一种先进的质检工具&#xff0c;虽然具有广泛的应用范围&#xff0c;但并非所有产品都适合进行视觉检测。本文将探讨视觉检测系统的适用范围及其局限性。 随着机器视觉技术的快速发展&#xff0c;视觉检测系统已广泛应用于各个行业&#xff0c;为产品质检提供…...

通过金山和微软虚拟打印机转换PDF文件,流程方法及优劣对比

文章目录 一、WPS/金山 PDF虚拟打印机1、常规流程2、PDF文件位置3、严重缺陷二、微软虚拟打印机Microsoft Print to Pdf1、安装流程2、微软虚拟打印机的优势一、WPS/金山 PDF虚拟打印机 1、常规流程 安装过WPS办公组件或金山PDF独立版的电脑,会有一个或两个WPS/金山 PDF虚拟…...

采用java+B/S开发的全套医院绩效考核系统源码springboot+mybaits 医院绩效考核系统优势

采用java开发的全套医院绩效考核系统源码springbootmybaits 医院绩效考核系统优势 医院绩效管理系统解决方案紧扣新医改形势下医院绩效管理的要求&#xff0c;以“工作量为基础的考核方案”为核心思想&#xff0c;结合患者满意度、服务质量、技术难度、工作效率、医德医风等管…...

驱动开发-用户空间和内核空间数据传输

1.用户空间-->内核空间&#xff08;写&#xff09; #include<linux/uaccess.h> int copy_from_user(void *to,const void __user volatile*from,unsigned long n) 函数功能&#xff1a;将用户空间数据拷贝到内核空间 参数&#xff1a; to&#xff1a;内核空间首地…...

【408精华知识】速看!各种排序的大总结!

文章目录 一、插入排序&#xff08;一&#xff09;直接插入排序&#xff08;二&#xff09;折半插入排序&#xff08;三&#xff09;希尔排序 二、交换排序&#xff08;一&#xff09;冒泡排序&#xff08;二&#xff09;快速排序 三、选择排序&#xff08;一&#xff09;简单选…...

【STM32 |程序实例】按键控制、光敏传感器控制蜂鸣器

目录 前言 按键控制LED 光敏传感器控制蜂鸣器 前言 上拉输入&#xff1a;若GPIO引脚配置为上拉输入模式&#xff0c;在默认情况下&#xff08;GPIO引脚无输入&#xff09;&#xff0c;读取的GPIO引脚数据为1&#xff0c;即高电平。 下拉输入&#xff1a;若GPIO引脚配置为下…...

Spring boot使用websocket实现在线聊天

maven依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spr…...

品牌设计理念和logo设计方法

一 品牌设计的目的 设计是为了传播&#xff0c;让传播速度更快&#xff0c;传播效率更高&#xff0c;减少宣传成本 二 什么是好的品牌设计 好的设计是为了让消费者更容易看懂、记住的设计&#xff0c; 从而辅助传播&#xff0c; 即 看得懂、记得住。 1 看得懂 就是让别人看懂…...

Python | Leetcode Python题解之第88题合并两个有序数组

题目&#xff1a; 题解&#xff1a; class Solution:def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:"""Do not return anything, modify nums1 in-place instead."""p1, p2 m - 1, n - 1tail m n - 1whi…...

vscode新版本remotessh服务端报`GLIBC_2.28‘ not found解决方案

问题现象 通过vscode的remotessh插件连接老版本服务器&#xff08;如RHEL7&#xff0c;Centos7&#xff09;时&#xff0c;插件会报错&#xff0c;无法连接。 查看插件的错误日志可以看到类似如下的报错信息&#xff1a; dc96b837cf6bb4af9cd736aa3af08cf8279f7685/node: /li…...

盘他系列——oj!!!

1.Openjudge 网站: OpenJudge 2.洛谷 网站: 首页 - 洛谷 | 计算机科学教育新生态 3.环球OJ 网站: QOJ - QOJ.ac 4. 北京大学 OJ:Welcome To PKU JudgeOnline 5.自由OJ 网站: https://loj.ac/ 6.炼码 网站:LintCode 炼码 8.力扣 网站: 力扣 9.晴练网首页 - 晴练网...

洛谷 P2657 [SCOI2009] windy 数 题解 数位dp

[SCOI2009] windy 数 题目背景 windy 定义了一种 windy 数。 题目描述 不含前导零且相邻两个数字之差至少为 2 2 2 的正整数被称为 windy 数。windy 想知道&#xff0c;在 a a a 和 b b b 之间&#xff0c;包括 a a a 和 b b b &#xff0c;总共有多少个 windy 数&…...

Python爬虫入门:网络世界的宝藏猎人

今天阿佑将带你踏上Python的肩膀&#xff0c;成为一名网络世界的宝藏猎人&#xff01; 文章目录 1. 引言1.1 简述Python在爬虫领域的地位1.2 阐明学习网络基础对爬虫的重要性 2. 背景介绍2.1 Python语言的流行与适用场景2.2 网络通信基础概念及其在数据抓取中的角色 3. Python基…...

【NodeMCU实时天气时钟温湿度项目 6】解析天气信息JSON数据并显示在 TFT 屏幕上(心知天气版)

今天是第六专题&#xff0c;主要内容是&#xff1a;导入ArduinoJson功能库&#xff0c;借助该库解析从【心知天气】官网返回的JSON数据&#xff0c;并显示在 TFT 屏幕上。 如您需要了解其它专题的内容&#xff0c;请点击下面的链接。 第一专题内容&#xff0c;请参考&a…...