Go语言系统学习笔记(三):杂项篇
公司的新业务开发需要用到go语言,虽然之前没接触过这门语言,但在大模型的帮助下,边看项目边写代码也能进行go的项目开发,不过,写了一段时间代码之后,总感觉对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 string
age 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/http | HTTP 相关服务,主要包括 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 = 0
for i := 0; i < 1000; i++ {
go func(idx int) {
a += 1
fmt.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 = 0
var lock sync.Mutex
for i := 0; i < 1000; i++ {
go func(idx int) {
lock.Lock() // 加锁
defer lock.Unlock() // 函数执行完了之后释放锁
a += 1
fmt.Printf("goroutine %d, a=%d\n", idx, a)
}(i)
}
time.Sleep(time.Second)
}
// 一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定)
读写锁有如下四个方法:
- 写操作的锁定和解锁分别是
func (*RWMutex) Lock
和func (*RWMutex) Unlock
; - 读操作的锁定和解锁分别是
func (*RWMutex) Rlock
和func (*RWMutex) RUnlock
。
读写锁的区别在于:
- 当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;
- 当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;
- 当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。
所以说这里的读锁定(RLock)目的其实是告诉写锁定,有很多协程或者进程正在读取数据,写操作需要等它们读(读解锁)完才能进行写(写锁定)。即
- 同时只能有一个 goroutine 能够获得写锁定;
- 同时可以有任意多个 gorouinte 获得读锁定;
- 同时只能存在写锁定或读锁定(读和写互斥)
理解起来就是:
- 如果有人在写的时候, 其他人读和写都不行, 因为其他人如果读, 可能在写的前后读的数据不一致,产生幻读,其他人如果写,那造成写冲突
- 有人在读的时候,其他人可以读,但是不能写,防止别人读错, 因为写可能会导致正在读的人读错
package main
import (
"fmt"
"math/rand"
"sync"
)
// count的全局变量用于读写操作
var count int
// 读写锁实例
var rw sync.RWMutex
func main() {
// 搞一个通道,容量为10
ch := 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 // 读数据count
fmt.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) // 修改count
count = v
fmt.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 over
func 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() {
<-ch1
ch2 <- 1
}()
go func() {
<-ch2
ch1 <- 1
}()
// 这会导致死锁,因为两个goroutine都在等待对方发送数据
}
// 活锁是指程序没有被阻塞(即执行单元都在运行),但是由于某种条件始终不能得到满足,导致程序无法向前推进。
// 虽然线程或goroutines还在继续执行,但是它们却做不了任何有用的工作。
// 场景: 两个goroutine彼此都在尝试避让对方(例如,基于某些条件重试操作,但这些条件却令彼此永远无法继续)
func main() {
ch := make(chan int)
done := make(chan bool)
var sharedResource int
go func() { // Goroutine 1试图增加sharedResource到一个特定的值
for sharedResource < 10 {
select {
case val := <-ch:
sharedResource += val
default:
fmt.Println("Goroutine 1等待增加")
time.Sleep(100 * time.Millisecond) // 等待资源,希望稍后能增加它
}
}
done <- true
}()
go func() { // Goroutine 2试图减少sharedResource到一个特定的值
for sharedResource > -10 {
select {
case val := <-ch:
sharedResource -= val
default:
fmt.Println("Goroutine 2等待减少")
time.Sleep(100 * time.Millisecond) // 等待资源,希望稍后能减少它
}
}
done <- true
}()
go func() { // Goroutine 3周期性地尝试发送变化到资源
for {
select {
case ch <- 1: // 尝试发送1到channel
fmt.Println("发送1")
case ch <- 2: // 尝试发送2到channel
fmt.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.WaitGroup
const runtime = 1 * time.Second
var sharedLock sync.Mutex
greedyWorker := func() {
defer wg.Done()
var count int
for 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 int
for 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 string
Age 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 string
Age 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]string
File1, _ := 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\n
str2 := "zhangsan\n"
str3 := "lisi\n"
//写入时,使用带缓存的 *Writer
writer := 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的开发,碰到新知识,就大概知道是哪块的内容,知道和哪些内容有关联了,然后再补充到对应的体系里面,慢慢完善就好。
我还是比较喜欢这种体系框架式的学习方式,这样能让碎片化的知识有关联, 更容易理解和记录,学习知识,不能孤立的学习某个点,得想办法联系到已有的知识框架中,然后横向和纵向反复和其他点作对比,才能更好的消化它, 如果没有合适的知识框架放,就得需要突击一段时间学习,整理一个初版的框架出来,这个突击不要求能把知识学习多么深入,只是为了先摸一遍,看看到底有啥,先有个大概,然后在实践里面去完善架子,慢慢的就有新的体系。 这是我理想里面的知识“大同”, 很多领域,很多知识,其实底层是相通的, 是有关联的,还得继续多读书,多学习,多扩充知识的广度与深度,慢慢的就会有感觉, 加油呀 😉

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。
更多推荐
所有评论(0)