go常用特性及常用包
1 常用特性
1.1 go:build
//go:build !windows
//go:build是前缀指令,!windows是逻辑判断的条件。这个指令的作用是在Windows系统外,编译当前源文件。// +build !windows
// +build是前缀指令,!windows是编译标记。这个指令的作用是告诉编译器只有当编译标记中不包含 windows时,才会编译当前源文件。
综合上述两个指令的作用,只有在非Windows系统下编译这个源文件时,会将其编译进目标可执行文件中。
- //go:build 386 && windows
- // +build 386,windows
作用:只有当操作系统为windows,同时arch架构为386时,才编译当前文件
1.2 go:embed
//go:embed指令是Go 1.16版本新增的官方命令,可以用于在可执行文件中嵌入文件。
指令格式有三种形式:
- //go:embed path…
- //go:embed regexp
- //go:embed dir/*.ext
其中:
- path… 是需要嵌入的文件或目录,可以为多个,用空格分隔。
- regexp 是需要嵌入的文件名或目录名的正则表达式。
- dir/*.ext 是需要嵌入的某个目录下特定扩展名的文件。
示例
假设我们有一个文件叫 data.txt,然后我们希望在程序中引用它,通过 //go:embed 指令即可嵌入。
package mainimport ("embed""fmt"
)//go:embed data.txt
var data stringfunc main() {fmt.Println(data)
}
在这个示例中,我们使用 //go:embed data.txt 将 data.txt 文件嵌入到了可执行文件中,并将其作为字符串赋值给了 var data string,然后在 main() 函数中输出了 data 变量的值。
1.3 其他
- go:noinline:禁止编译器进行内联,即使启用了 -l 标志也不会内联。
- go:noescape:告诉编译器某个函数或方法没有任何指针可以逃逸到外部,可以更好地进行优化。
- go:linkname:用于跨包调用未导出的函数。
- go:cgo_export_static、//go:cgo_export_dynamic、//go:cgo_import_static、//go:cgo_import_dynamic:用于在Go和C语言之间交互数据。
1.4 插件化开发
Golang官方提供了plugin模块,该模块可以支持插件开发.
目前很多思路都是在开发过程中支持插件话,当主体程序写完后,不能够临时绑定插件.但是本文将带领你进行主体程序自动识别并加载、控制插件调用.
1 基本思路
插件化开发中,一定存在一个主体程序,对其他插件进行控制、处理、调度.
1.1 基本业务
- 我们首先开发一个简单的业务程序,进行两种输出.
- 当时间秒数为奇数的时候,输出hello
- 当时间秒数为偶数的时候,输出world
主体代码,MainFile.go:
package mainimport ("fmt""time"
)// init 函数将于 main 函数之前运行
func init() {fmt.Println("Process On ==========")
}func main() {// time.Now().Second 将会返回当前秒数nowSecond := time.Now().Second()doPrint(nowSecond)fmt.Println("Process Stop ========")
}// 执行打印操作
func doPrint(nowSecond int) {if nowSecond%2 == 0 {printWorld() //偶数} else {printHello() //奇数}
}func printHello() {fmt.Println("hello")
}func printWorld() {fmt.Println("world")
}
代码有一定的冗余,是为了模拟业务之间的调度
运行代码:
1.2 编写简单插件
然后我们编写一个插件代码,
插件代码的入口package也要为main
,但是可以不包含main方法
- 设定插件逻辑为当当前秒数为奇数的时候,同时输出当前时间(与hello的判定不是一个时间)
- 插件文件名:HelloPlugin.go
在当前目录下,执行插件生成指令:
注意:
buildmode=plugin 只支持在Linux和Mac系统下的64位构建,并不支持在Windows系统下的64位构建。
// mac或者linux系统
go build --buildmode=plugin -o HelloPlugin.so HelloPlugin.go
当前目录下就会多出来一个文件HelloPlugin.so,然后,我们让主程序加载该插件
package mainimport ("fmt""plugin""time"
)// 定义插件信息
const pluginFile = "HelloPlugin.so"// 存储插件中将要被调用的方法或变量
var pluginFunc plugin.Symbol// init 函数将于 main 函数之前运行
func init() {// 查找插件文件pluginFile, err := plugin.Open(pluginFile)if err != nil {fmt.Println("An error occurred while opening the plug-in")} else {// 查找目标函数targetFunc, err := pluginFile.Lookup("PrintNowTime")if err != nil {fmt.Println("An error occurred while search target func")}pluginFunc = targetFunc}fmt.Println("Process On ==========")
}func main() {// time.Now().Second 将会返回当前秒数nowSecond := time.Now().Second()doPrint(nowSecond)fmt.Println("Process Stop ========")
}func doPrint(nowSecond int) {if nowSecond%2 == 0 {printWorld() //偶数} else {printHello() //奇数}
}func printHello() {// 执行插件调用if pluginFunc != nil {//将存储的信息转换为函数if targetFunc, ok := pluginFunc.(func()); ok {targetFunc()}}fmt.Println("hello")
}func printWorld() {fmt.Println("world")
}
运行代码:
2 常用包
2.1 标准库
文档地址:http://doc.golang.ltd/
①os模块
1 文件目录相关
//【1】创建文件
file, err := os.Create("file.txt")//【2】创建目录
err := os.Mkdir("test2", os.ModePerm) //单个目录
err := os.MkdirAll("/a/b/c", os.ModePerm) //层级目录//【3】删除文件或目录
err := os.Remove("test.txt")
err = os.RemoveAll("test2")//【4】获取工作目录
dir, err := os.Getwd()//【5】修改工作目录
err := os.Chdir("d:/")//【6】读写文件
bytes, err := os.ReadFile("test2.txt")
os.WriteFile("test2.txt", []byte("hello go"), os.ModePerm)//【7】文件重命名
err := os.Rename("test2.txt", "test3.txt")//【8】读取目录列表
2 File文件读操作
//【1】打开文件
file, err := os.Open("a.txt) //如果a.txt不存在,则报错[打开的文件为只读]
file, err := os.OpenFile("a1.txt", os.O_RDWR|os.OCREATE, 755) //如果不存在则创建//【2】循环读取文件
f, _ := os.Open("a.txt")
for {buf := make([]byte, 3)n, err := f.Read(buf)//读到文件末尾if err == io.EOF {break}fmt.Printf("n:%v\n", n)fmt.Printf("string(buf):%v\n", string(buf))
}
f.Close()//【3】从指定位置读取
方法一:
f, _ := os.Open("a.txt")
buf := make([]byte, 4)
//从offset为3的位置开始读取
n, _ := f.ReadAt(buf, 3)
fmt.Println("n=", n)
fmt.Println("string(buf)=", string(buf))方法二:
file, _ := os.Open("test/a.txt")
defer file.Close()
//从偏移量为3的位置开始读取
file.Seek(3, 0)
buf := make([]byte, 10)
n, _ := file.Read(buf)
fmt.Println("n=", n)
fmt.Println("string(buf)=", string(buf))//【4】读取目录
dir, _ := os.ReadDir("a/")
for _, v := range dir {fmt.Printf("v.IsDir():%v\n", v.IsDir())fmt.Printf("v.Name():%v\n", v.Name())
}
3 File文件写操作
//os.O_TRUNC //覆盖之前的
//os.O_APPEND //追加写//【1】写入字节
file, _ := os.OpenFile("a.txt", os.O_RDWR|os.O_APPEND, 0775)
file.Write([]byte("hello golang"))
file.Close()//【2】写入字符串
file.WriteString("hello java")//【3】从指定位置写
//从file的offset为3的位置开始写
file.WriteAt([]byte("aaa"), 3)
4 进程相关操作
package mainimport ("fmt""os""time"
)func main() {//获取当前正在运行的进程idfmt.Printf("os.Getpid():%v\n", os.Getpid())//父idfmt.Printf("os.Getppid():%v\n", os.Getppid())//设置新进程的属性attr := &os.ProcAttr{//files指定新进程继承的活动文件对象//前三个分别为:标准输入、标准输出、标准错误输出Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},//新进程的环境变量Env: os.Environ(),}//开始一个新进程p, err := os.StartProcess("D:\\Download\\EditPlus\\EditPlus.exe", []string{"D:\\Download\\EditPlus\\EditPlus.exe", "E:\\processDemo.txt"}, attr)if err != nil {fmt.Println(err)}fmt.Println(p)fmt.Println("进程ID:", p.Pid)//通过进程id查找进程p2, _ := os.FindProcess(p.Pid)fmt.Println(p2)//等待5s,执行函数time.AfterFunc(time.Second*5, func() {//向进程p发送退出信号【杀死进程】p.Signal(os.Kill)})//等待进程p的退出,返回进程状态ps, _ := p.Wait()fmt.Println(ps.String())}
运行结果:
os.Getpid():19828
os.Getppid():22092
&{20312 372 0 {{0 0} 0 0 0 0}}
进程ID: 20312
&{20312 352 0 {{0 0} 0 0 0 0}}
exit status 1
5 环境变量
//【1】获取和设置
//获取所有环境变量
s := os.Environ()
fmt.Printf("s:%v\n", s)
//获取某个环境变量
s2 := os.Getenv("GOPATH")
fmt.Printf("s2:%v\n", s2)
//获取不存在的环境变量[获取的结果为空,不会报错;如果要看环境变量是否存在;推荐使用LookupEnv]
s2 = os.Getenv("lalala")
//设置环境变量
os.Setenv("env1", "env1Value")//【2】查找
s3, b := os.LookupEnv("env1")
if b {fmt.Println("s3=", s3)
}//【3】清空环境变量,慎用!!!!
//os.Clearenv()
②io包、ioutil包、bufio
1 io包
哪些类型实现了Reader和Writer接口:
- os.File
- string.Reader
- bufio.Reader
- bytes.Buffer
- bytes.Reader
- compress/gzip.Reader/Writer
- encoding/csv.Reader/Writer
简单测试案例:
func main() {r := strings.NewReader("hello world")//os.Stdout返回的也是一个Writer_, err := io.Copy(os.Stdout, r)if err != nil {fmt.Println(err)}//控制台输出:hello world
}
2 iotuil包
名称 | 作用 |
---|---|
ReadAll | 读取数据,返回读到的字节 |
ReadDir | 读取一个目录,返回目录入口数组[]os.FileInfo |
ReadFile | 读取一个文件,返回文件内容(字节slice) |
WriteFile | 根据文件路径,写入字节slice |
TempDir | 在一个目录中创建指定前缀名的临时目录,返回新临时目录的路径 |
TempFile | 在一个目录中创建指定前缀名的临时文件,返回os.File |
简单案例:
func main() {fi, _ := ioutil.ReadDir(".")for _, v := range fi {if v.IsDir() {fmt.Println("dir=", v.Name())} else {fmt.Println("file=", v.Name())}}
}
3 bufio包
涉及到其他语言,如:中文,直接使用rune转换即可
Reader操作:
func main() {file, _ := os.Open("test/test1/test2/test.csv")defer file.Close()br := bufio.NewReader(file)buffer := make([]byte, 10)for {n, err := br.Read(buffer)if err == io.EOF {break} else {fmt.Println("value=", string(buffer[:n]))}}
}
- Writer操作
func main() {//写入文件的话,需要使用OpenFile,并设置对应写入权限file, _ := os.OpenFile("test/test1/test2/test.csv", os.O_RDWR, 0777)defer file.Close()w := bufio.NewWriter(file)w.Write([]byte("hahaha~~~"))w.Flush()
}
- Scanner
func main() {s := strings.NewReader("ABC DEF KIS")bs := bufio.NewScanner(s)//以空格作为分隔符bs.Split(bufio.ScanWords)for bs.Scan() {fmt.Println(bs.Text())}
}
③path/filepath
- Rel
func Rel(basepath, targpath string) (string, error)
该函数以basepath为基准,返回targpath相对于basepath的相对路径,也就是说如果basepath为/a,targpath为/a/b/c,那么则会返回/b/c,如果两个参数有一个为绝对路径,一个为相对路径,则会返回错误
- Join
func Join(elem ...string) string
Join 函数将多个路径进行连接,并且进行Clean操作,然后返回
④archive/zip包
- 压缩
package mainimport ("archive/zip""fmt""io""os""path/filepath""strings"
)func compressionDir(baseDir string) (string, error) {zipFileName := baseDir + ".zip"// 创建一个新的 zip 文件zipFile, err := os.Create(zipFileName)if err != nil {return "", err}defer zipFile.Close()// 创建一个 zip.WriterzipWriter := zip.NewWriter(zipFile)defer zipWriter.Close()// 遍历目录下的所有文件和子目录err = filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {if err != nil {return err}// 创建一个 zip 文件中的文件或目录relativePath := strings.TrimPrefix(path, baseDir)zipPath := strings.TrimLeft(filepath.Join("/", relativePath), "/")// 如果是目录或空目录,则在 zip 文件中创建一个目录if info.IsDir() || isEmptyDir(path) {_, err := zipWriter.Create(zipPath + "/")if err != nil {return err}} else {// 如果是文件,则创建一个 zip 文件中的文件zipFile, err := zipWriter.Create(zipPath)if err != nil {return err}// 打开原始文件file, err := os.Open(path)if err != nil {return err}defer file.Close()// 将原始文件的内容拷贝到 zip 文件中_, err = io.Copy(zipFile, file)if err != nil {return err}}return nil})if err != nil {return "", err}return zipFileName, nil
}// 判断目录是否为空目录
func isEmptyDir(dirPath string) bool {dir, err := os.Open(dirPath)if err != nil {return false}defer dir.Close()_, err = dir.Readdirnames(1)return err == io.EOF
}func main() {// 调用压缩函数zipFile, err := compressionDir("E:\\Go\\GoPro\\src\\go_code\\gouitest\\test")if err != nil {fmt.Println("压缩目录失败:", err)return}fmt.Println("目录压缩成功,压缩文件:", zipFile)
}
运行结果:
- 解压
2.2 并发
①Mutex
- 互斥锁
- 不可重入锁
sync.Mutex
是Go语言中的一个同步原语,用于实现互斥锁。它可以保证在同一个时刻只有一个goroutine可以访问共享资源,从而避免了竞态条件和数据竞争。
sync.Mutex的用法非常简单,我们可以通过调用Lock方法来获取锁,在获取锁之后访问共享资源,然后通过调用Unlock
方法释放锁。如果在获取锁之前锁已经被其他goroutine获取了,那么当前goroutine将会被阻塞,直到被释放为止。
现在我们通过sync.Mutex来保证并发访问资源安全
- 如果不使用sync.Mutex
package mainimport ("fmt""sync""time"
)var (i = 100wg sync.WaitGroup
)func main() {for i := 0; i < 50; i++ {wg.Add(1)go add()wg.Add(1)go sub()}//通过waitGroup等待所有任务跑完wg.Wait()fmt.Println("main....i=", i)
}func add() {time.Sleep(time.Millisecond * 10)defer wg.Done()i += 1fmt.Println("i++, i=", i)
}func sub() {time.Sleep(time.Millisecond * 2)defer wg.Done()i -= 1fmt.Println("i--, i=", i)
}
正确结果应该是i=100,因此可以知道结果是错误的,因为我们没有控制并发。接下来,我们通过sync.Mutex互斥锁来控制并发
- 使用sync.Mutex控制并发
package mainimport ("fmt""sync""time"
)var (i = 100wg sync.WaitGrouplock sync.Mutex
)func main() {for i := 0; i < 50; i++ {wg.Add(1)go add()wg.Add(1)go sub()}//通过waitGroup等待所有任务跑完wg.Wait()fmt.Println("main....i=", i)
}func add() {defer wg.Done()//访问共享资源的时候加锁lock.Lock()time.Sleep(time.Millisecond * 10)i += 1fmt.Println("i++, i=", i)lock.Unlock()
}func sub() {defer wg.Done()lock.Lock()time.Sleep(time.Millisecond * 2)i -= 1fmt.Println("i--, i=", i)lock.Unlock()
}
现在,不管我们怎么运行就都可以获取到正确的结果了
对于sync.Mutex能否unlock多次的问题,答案是不能。如果在没有获取锁的情况下调用unlock方法,或者在已经释放锁的情况下再次调用unlock方法,都会导致运行时错误。因此,在使用sync.Mutex
时,需要确保每次获取锁之后都能及时释放锁,避免出现这种问题。
package mainimport "sync"var (mutex sync.Mutex
)func main() {/*【1】加锁一次,解锁两次=》报错mutex.Lock()mutex.Unlock()mutex.Unlock()//fatal error: sync: unlock of unlocked mutex*//*【2】加锁两次,解锁一次=》报错mutex.Lock()mutex.Lock()mutex.Unlock()//fatal error: all goroutines are asleep - deadlock!*//*【3】先连续加锁两次,然后连续解锁两次=》报错 `sync.Mutex是不可重入锁`mutex.Lock()mutex.Lock()mutex.Unlock()mutex.Unlock()//fatal error: all goroutines are asleep - deadlock!*/// 【4】可行,只有先加锁,然后释放锁之后才能继续加锁mutex.Lock()mutex.Unlock()mutex.Lock()mutex.Unlock()
}
- 拓展:
可重入锁
- 当一个线程获取锁时,如果没有其它线程拥有这个锁,那么,这个线程就成功获取到这个锁。之后,如果其它线程再请求这个锁,就会处于阻塞等待的状态。但是,如果拥有这把锁的线程再请求这把锁的话,不会阻塞,而是成功返回,所以叫可重入锁。只要你拥有这把锁,你可以可着劲儿地调用,比如通过递归实现一些算法,调用者不会阻塞或者死锁。
- Mutex 不是可重入的锁。Mutex 的实现中没有记录哪个 goroutine 拥有这把锁。理论上,任何 goroutine 都可以随意地 Unlock 这把锁,所以没办法计算重入条件。
package mainimport ("fmt""sync""time"
)var (mutex sync.Mutexcount int
)func main() {go func() {mutex.Lock()count++}()time.Sleep(time.Second * 1)mutex.Unlock()fmt.Println("main.....")fmt.Println("count=", count)//main.....//count= 1
}
可以看到我们通过一个协程(goroutine)加的锁,但是可以在main方法中释放(main方法也可以看做一个特殊的goroutine)中释放,因此可以知道sync.Mutex不会记录哪个goroutine持有这把锁。
②WaitGroup
两个协程之间相互等待,如果没有waitGroup,可能协程里面的任务还没有完成,主程序就退出了,导致所有的协程也退出
package mainimport ("fmt""sync"
)var (wg sync.WaitGroup
)func hello(i int) {defer wg.Done() //wd.Add(-1)fmt.Println("hello", i)//wg.Done() //wd.Add(-1)
}func main() {for i := 0; i < 10; i++ {wg.Add(1)go hello(i)}//等待所有协程中的任务全部完成wg.Wait()fmt.Println("main====")
}
③Timer、Ticker
1 Timer
定时器,timer.C本质上是一个管道
package mainimport ("fmt""time"
)func main() {//[1]time.NewTimer 等待两秒钟//timer := time.NewTimer(time.Second * 2)//t := <-timer.C//fmt.Println(t)//[2]time.After(time.Second * 2) 等待两秒钟//time.After(time.Second * 2)//[3]timer := time.NewTimer(time.Second * 5)timer.Reset(time.Second * 6) //重新设置定时器时间//<-timer.Ctimer.Stop() //停止定时器(如果没有timer.C 那么就不会阻塞暂停)fmt.Println("--")
}
2 Ticker
Timer只执行一次,Ticker可以周期的执行
案例一:
package mainimport ("fmt""time"
)func main() {fmt.Println("start...")//定时器,每隔5秒执行ticker := time.NewTicker(time.Second * 5)for _ = range ticker.C {fmt.Println("middle...")}fmt.Println("end...") //永远不会执行到,因为ticker.C是可以定时器,for中没有break
}
案例二:
package mainimport ("fmt""time"
)func main() {//定时器,每隔一秒执行ticker := time.NewTicker(time.Second)chanInt := make(chan int)go func() {//ticker.C触发for _ = range ticker.C {select {case chanInt <- 1:case chanInt <- 2:case chanInt <- 3:}}}()sum := 0for v := range chanInt {fmt.Println("接收到:", v)sum += vif sum >= 10 {break}}
}
运行结果:
接收到: 2
接收到: 2
接收到: 1
接收到: 3
接收到: 1
接收到: 2
④runtime
让出CPU时间片,重新安排任务
- runtime.Gosched():让出时间片,让其他协程来执行
- runtime.Goexit():直接退出当前协程
- runtime.NumCPU():获取当前系统的cpu核心数
- runtime.GOMAXPROCS(num int):设置当前系统cpu核心数(默认为当前系统最大cpu核心数)
⑤原子变量
在并发操作资源时,我们可以通过两种方式来保证数据正确:
- 加锁
- 原子操作
atomic常见操作有:
- 增减
- 载入 read
- 比较并交换 cas
- 交换
- 存储 write
package mainimport ("fmt""sync/atomic"
)func main() {//test_add_sub()//test_load_store()test_cas()//除了cas比较并交换,atomic还有比较暴力的`直接交换`,但是这种用法比较少
}func test_add_sub() {var i int32 = 100atomic.AddInt32(&i, 1)fmt.Println("i=", i)atomic.AddInt32(&i, -1)fmt.Println("i=", i)
}func test_load_store() {var j int64 = 200val := atomic.LoadInt64(&j) //readfmt.Println("val=", val)atomic.StoreInt64(&j, -100) //writefmt.Println("j=", j)
}func test_cas() {var k int32 = 8f := atomic.CompareAndSwapInt32(&k, 8, 100)fmt.Println("flag=", f)fmt.Println("k=", k)
}
2.3 操作数据库
以MySQL为例,首先需要安装MySQL8版本,然后导入go操作MySQL的依赖库
- 地址:https://pkg.go.dev/github.com/go-sql-driver/mysql#section-readme
- 也可以直接通过在终端执行go get下载
go get -u github.com/go-sql-driver/mysql
package mainimport ("database/sql""fmt"_ "github.com/go-sql-driver/mysql"
)var db *sql.DBfunc initDB() (err error) {dataSource := "root:200151@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=true"//不会校验账号密码是否正确//注意!!不要使用:=,因为我们这里是给全局变量赋值,然后在main函数中使用全局变量dbdb, err = sql.Open("mysql", dataSource)if err != nil {return err}//尝试与数据库建立连接(校验dataSource是否正确)err = db.Ping()if err != nil {return err}return nil
}func main() {err := initDB()if err != nil {fmt.Println("initDB fail, err=", err)} else {fmt.Println("连接成功!")}//以插入操作为例【其他操作类似】s := "insert into account (name, money, question) values (?, ?, ?)"result, err := db.Exec(s, "ziyi", 300.0, "what")if err != nil {fmt.Println("insert fail err=", err)} else {//insert success {0xc000102000 0xc00009ea00}fmt.Println("insert success ", result)}
}
教程:https://duoke360.com/tutorial/golang