go常用特性(embed、插件化开发)、常用包(并发)

news2024/11/25 2:05:32

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版本新增的官方命令,可以用于在可执行文件中嵌入文件。

指令格式有三种形式:

  1. //go:embed path…
  2. //go:embed regexp
  3. //go:embed dir/*.ext

其中:

  • path… 是需要嵌入的文件或目录,可以为多个,用空格分隔。
  • regexp 是需要嵌入的文件名或目录名的正则表达式。
  • dir/*.ext 是需要嵌入的某个目录下特定扩展名的文件。

示例

假设我们有一个文件叫 data.txt,然后我们希望在程序中引用它,通过 //go:embed 指令即可嵌入。

package main

import (
    "embed"
    "fmt"
)

//go:embed data.txt
var data string

func 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 基本业务

  • 我们首先开发一个简单的业务程序,进行两种输出.
  1. 当时间秒数为奇数的时候,输出hello
  2. 当时间秒数为偶数的时候,输出world

主体代码,MainFile.go:

package main

import (
	"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 main

import (
	"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 main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	//获取当前正在运行的进程id
	fmt.Printf("os.Getpid():%v\n", os.Getpid())
	//父id
	fmt.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

  1. Rel
func Rel(basepath, targpath string) (string, error)

该函数以basepath为基准,返回targpath相对于basepath的相对路径,也就是说如果basepath为/a,targpath为/a/b/c,那么则会返回/b/c,如果两个参数有一个为绝对路径,一个为相对路径,则会返回错误

  1. Join
func Join(elem ...string) string

Join 函数将多个路径进行连接,并且进行Clean操作,然后返回

④archive/zip包

  1. 压缩
package main

import (
	"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.Writer
	zipWriter := 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)
}

运行结果:
在这里插入图片描述

  1. 解压

2.2 并发

①Mutex

- 互斥锁
- 不可重入锁

sync.Mutex是Go语言中的一个同步原语,用于实现互斥锁。它可以保证在同一个时刻只有一个goroutine可以访问共享资源,从而避免了竞态条件和数据竞争。

sync.Mutex的用法非常简单,我们可以通过调用Lock方法来获取锁,在获取锁之后访问共享资源,然后通过调用Unlock方法释放锁。如果在获取锁之前锁已经被其他goroutine获取了,那么当前goroutine将会被阻塞,直到被释放为止。

现在我们通过sync.Mutex来保证并发访问资源安全

  • 如果不使用sync.Mutex
package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	i  = 100
	wg 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 += 1
	fmt.Println("i++, i=", i)
}

func sub() {
	time.Sleep(time.Millisecond * 2)
	defer wg.Done()
	i -= 1
	fmt.Println("i--, i=", i)
}

在这里插入图片描述

正确结果应该是i=100,因此可以知道结果是错误的,因为我们没有控制并发。接下来,我们通过sync.Mutex互斥锁来控制并发

  • 使用sync.Mutex控制并发
package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	i    = 100
	wg   sync.WaitGroup
	lock 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 += 1
	fmt.Println("i++, i=", i)
	lock.Unlock()
}

func sub() {
	defer wg.Done()
	lock.Lock()
	time.Sleep(time.Millisecond * 2)
	i -= 1
	fmt.Println("i--, i=", i)
	lock.Unlock()
}

现在,不管我们怎么运行就都可以获取到正确的结果了

对于sync.Mutex能否unlock多次的问题,答案是不能。如果在没有获取锁的情况下调用unlock方法,或者在已经释放锁的情况下再次调用unlock方法,都会导致运行时错误。因此,在使用sync.Mutex时,需要确保每次获取锁之后都能及时释放锁,避免出现这种问题。

package main

import "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 main

import (
	"fmt"
	"sync"
	"time"
)

var (
	mutex sync.Mutex
	count 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 main

import (
	"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 main

import (
	"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.C
	timer.Stop() //停止定时器(如果没有timer.C 那么就不会阻塞暂停)
	fmt.Println("--")
}

2 Ticker

Timer只执行一次,Ticker可以周期的执行

案例一:

package main

import (
	"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 main

import (
	"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 := 0
	for v := range chanInt {
		fmt.Println("接收到:", v)
		sum += v
		if sum >= 10 {
			break
		}
	}
}

运行结果:

接收到: 2
接收到: 2
接收到: 1
接收到: 3
接收到: 1
接收到: 2

④runtime

让出CPU时间片,重新安排任务

  • runtime.Gosched():让出时间片,让其他协程来执行
  • runtime.Goexit():直接退出当前协程
  • runtime.NumCPU():获取当前系统的cpu核心数
  • runtime.GOMAXPROCS(num int):设置当前系统cpu核心数(默认为当前系统最大cpu核心数)

⑤原子变量

在并发操作资源时,我们可以通过两种方式来保证数据正确:

  1. 加锁
  2. 原子操作

atomic常见操作有:

  • 增减
  • 载入 read
  • 比较并交换 cas
  • 交换
  • 存储 write
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	//test_add_sub()
	//test_load_store()
	test_cas()
	//除了cas比较并交换,atomic还有比较暴力的`直接交换`,但是这种用法比较少
}

func test_add_sub() {
	var i int32 = 100
	atomic.AddInt32(&i, 1)
	fmt.Println("i=", i)
	atomic.AddInt32(&i, -1)
	fmt.Println("i=", i)
}

func test_load_store() {
	var j int64 = 200
	val := atomic.LoadInt64(&j) //read
	fmt.Println("val=", val)
	atomic.StoreInt64(&j, -100) //write
	fmt.Println("j=", j)
}

func test_cas() {
	var k int32 = 8
	f := 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 main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)

var db *sql.DB

func initDB() (err error) {
	dataSource := "root:200151@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=true"
	//不会校验账号密码是否正确
	//注意!!不要使用:=,因为我们这里是给全局变量赋值,然后在main函数中使用全局变量db
	db, 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CSDN如何获得铁粉?

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

springboot+vue的校园疫情防控系统(附数据库,源码)

&#x1f495;&#x1f495;作者&#xff1a;程序员徐师兄 个人简介&#xff1a;7 年大厂程序员经历&#xff0c;擅长Java、微信小程序、Python、Android等&#xff0c;大家有这一块的问题可以一起交流&#xff01; 各类成品java毕设 。javaweb&#xff0c;ssh&#xff0c;ssm&…

责任链模式(二十六)

相信自己&#xff0c;请一定要相信自己 上一章简单介绍了策略模式(二十五), 如果没有看过, 请观看上一章 一. 责任链模式 引用 菜鸟教程里面的责任链模式介绍: https://www.runoob.com/design-pattern/chain-of-responsibility-pattern.html 顾名思义&#xff0c;责任链模式…

Science Advance||个体动态脑中鲁棒的大脑状态

文章目录 个体化动态方法&#xff08;INSCAPE 方法&#xff09;&#xff1a;&#xff08;A&#xff09;生成脑共同激活状态的组模板&#xff1a;&#xff08;B&#xff09;个体水平分析&#xff1a; 不同的大脑状态有特定的协同激活模式&#xff08;coactivation patterns&…

ai写作怎么搞?我来教你几招

在当今信息化时代&#xff0c;ai技术的发展已经进入到了一个全新的阶段&#xff0c;越来越多的人们开始运用ai技术进行写作。作为一种创新性的工具&#xff0c;ai写作已经成为了许多企业和个人写作必不可少的工具之一。但是&#xff0c;对于初学者来说&#xff0c;如何快速上手…

【网络技术】NAT是什么?它的工作原理是什么?

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 作者会持续更新网络知识和python基础知识&#xff0c;期待你的关注 目录 一、NAT是什么&#xff1f; 二、NAT的实现方式&#xff08;三种&#xff09; 1、静态转换&#xff08;Static Nat&#xff09; 2、动…

css line-height

项目中看到 line-height&#xff1a;1 所以来总结一下 line-height 属性。 line-height 定义 line-height 属性设置行间的距离&#xff08;行高&#xff09;。 line-height 不允许使用负值。 属性可能的值 值描述normal默认。设置合理的行间距。number设置数字&#xff0…

手机技巧:安卓微信 8.0.38 内测版本功能一览

2023年6月14号安卓版本的微信8.0.38又开始内测了&#xff0c;今天就赶紧下载体验一下&#xff0c;下面就来给大家一一介绍&#xff0c;本次安卓微信内测版本功能更新&#xff0c;感兴趣的朋友可以文末下载体验一下&#xff01; 首先看一下官方的更新内容&#xff1a; 本次更新…

【Axure 教程】中继器(基础篇)

一、初识中继器 中继器是 Axure 中一个比较高阶的应用&#xff0c;它可以让我们在纯静态网页中模拟出类似带有后台数据交互的增删改查的效果&#xff0c;虽然它没有真正意义上帮我们存储任何的数据&#xff0c;但是当我们在一次项目体验过程中&#xff0c;它却可以给我们带来更…

字节跳动提出高性能 transformer 推理库,获 IPDPS 2023 最佳论文奖

动手点关注 干货不迷路 字节跳动与英伟达, 加州大学河滨分校联合发表的论文 《ByteTransformer: A High-Performance Transformer Boosted for Variable-Length》在第 37 届 IEEE 国际并行和分布式处理大会&#xff08;IPDPS 2023&#xff09;中&#xff0c;从 396 篇投稿中脱颖…

广东省高校人工智能产教融合院长研讨会召开,校企协同探索AI教育新范式

为深化产教融合、促进校企合作&#xff0c;着力推进人工智能产业和高校人才培养体系相融合&#xff0c;深入探讨校企合作、产教融合与课程建设规划等事宜&#xff0c;2023年6月9日下午&#xff0c;百度飞桨联合广东省计算机学会、华南理工大学计算机科学与工程学院、荔峰科技&a…

这世界好神奇,我们其实并不了解自己的身体

点击文末“阅读原文”即可参与节目互动 剪辑、音频 / 卷圈 运营 / SandLiu 卷圈 监制 / 姝琦 文案 / 粒粒 产品统筹 / bobo 场地支持 / 声湃轩北京站 这是一次突发奇想的天马行空&#xff0c;三个人猝不及防地坐下来大开脑洞&#xff0c;从诗词歌赋聊到人生哲学&#xff…

CVPR 2023 | 美团技术团队精选论文解读

本文精选了美团技术团队被CVPR 2023收录的8篇论文进行解读。这些论文既有自监督学习、领域自适应、联邦学习等通用学习范式方面的技术迭代&#xff0c;也涉及目标检测、跟踪、分割、Low-level Vision等典型视觉任务的性能&#xff0c;体现了美团在基础通用技术和垂直领域技术上…

HotSpot虚拟机对象探索与OutOfMemoryError异常

HotSpot虚拟机对象探索与OutOfMemoryError异常 1.HotSpot虚拟机对象探索 1.1对象的创建 不是一直有一个笑话,别人问程序员有没有对象,程序员会说我没有对象,但是我可以new一个出来 这里就可以判断他学过c或者java等语言 在java中对象的创建一般我们都是通过new来创建的,但…

MyBatis01

ORM&#xff1a;对象关系映射 O&#xff08;Object&#xff09;&#xff1a;Java虚拟机中的Java对象 R&#xff08;Relational&#xff09;&#xff1a;关系型数据库 M&#xff08;Mapping&#xff09;&#xff1a;将Java虚拟机中的Java对象映射到数据库表中一行记录&#xff0…

【王道·操作系统】第三章 内存管理

一、内存管理 1.1 内存的基础知识 内存可存放数据&#xff0c;程序执行前需要先放到内存中才能被CPU处理——缓和CPU与硬盘之间的速度矛盾内存地址从0开始&#xff0c;每个地址对应一个存储单元 按字节编址&#xff1a;每个存储单元大小为1字节(B)&#xff0c;即8个二进制位按…

【Spring Cloud系列】- Eureka使用详解

【Spring Cloud系列】- Eureka使用详解 文章目录 【Spring Cloud系列】- Eureka使用详解一、概述二、Eureka简介三、Eureka结构与作用Eureka结构图Eureka采用CS&#xff08;Client/Server,客户端/服务器&#xff09;架构&#xff0c;它包括以下两大组件 四、Eureka集群及与应用…

梁宁:为什么中国没有像 ChatGPT 和 Vision Pro 这样的创新产品?

6 月 10 日&#xff0c;产品战略专家梁宁和图灵联合创始人刘江围绕“ ChatGPT 真需求”主题进行直播对谈。 梁宁&#xff0c;产品战略专家&#xff0c;曾任湖畔大学产品模块学术主任&#xff0c;联想、腾讯高管&#xff0c;CNET集团副总裁。 工作经历横跨 BAT&#xff0c;与美团…

第14届蓝桥杯Scratch(中级)国赛真题解析2023.5.28

第14届蓝桥杯Scratch(中级)国赛真题解析2023.5.28 一:选择题(50分)第 1 题 单选题(10分) 运行以下程序后,角色说出的数是 ( C )。 *选择题严禁使用程序验证,选择题不答或答错都不扣分 A.150 B.200 C.300 D.600第 2 题 单选题(10分) 对以下程序效果描述完全正确的是 …

【JUC进阶】01. Synchroized实现原理

目录 1、前言 2、Synchronized使用 2.1、对象锁&#xff08;Instance Lock&#xff09; 2.2、类锁&#xff08;Class Lock&#xff09; 2.3、方法锁&#xff08;Method Lock&#xff09; 3、原理分析 3.1、monitor对象 3.2、monitorenter 3.3、monitorexit 3.4、对象…