Go,Gin学习
Go & Gin & Gorm 学习
Go
Learn go by test
基本语法
package:类似namespace或者module
循环
func Repeat(character string) string {
var repeat string
<!--truncate--> for i:=1;i<=5;i++{
repeat = repeat + character
}
return repeat
}
其中:=是带初始值定义的语法糖,var name type是不带初始值的定义;go 的循环只有 for
测试示例
import (
"testing"
)
func TestRepeat(t *testing.T){
got:=Repeat("a")
expect:="aaaaa"
if got!=expect{
t.Errorf("got %q, expect %q", got, expect)
}
}
使用
go test
运行测试,如果有定义 main 函数(在package main之中),则可以直接go run xx.go
go 包内函数和成员变量的可见性是通过大小 写区分的,大写对包外可见,小写不可见
test 函数需要写成TestXXX的形式,同时文件名也要写成whatfiles_test
基准测试 benchmark,BenchmarkXXX函数名
strings 库和相关函数
range 迭代
func Sum(arr []int) int {
sum := 0
// index, element
for _, ele := range arr{
sum += ele
}
return sum
}
go test -cover # 测试测试覆盖率
变长参数和 append
func SumAll(arrs ...[]int) []int{
ans:=[]int{}
for _, arr := range arrs{
ans = append(ans, Sum(arr))
}
return ans
}
函数指针(函数可以直接给变量)
checkSums := func(t testing.TB, got, want []int) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}
集合切片:go 支持类似 python 的切片
结构和方法(OOP)
import "math"
type Rectangle struct {
width float32
height float32
}
type Circle struct {
radius float32
center_x float32
center_y float32
}
func (r Rectangle) Area() float32 {
return r.width * r.height
}
func (c Circle) Area() float32 {
return math.Pi * c.radius * c.radius
}
go 不允许函数重载,类完全和结构体等价,类内函数(方法)如上定义
结构体可以嵌套,并且支持折叠调用(例如 A.B.ccc, ccc 是 B 的属性,B 是 A 的属性,可以直接 A.ccc)
go 有 interface
type Shape interface{
Area() float32
}
测试也方便
func TestArea(t *testing.T){
checkArea := func(t *testing.T, shape Shape, expect float32){
diff := shape.Area() - expect
if math.Abs(float64(diff)) > 1e-6{
t.Errorf("expect %v, got %v, diff %v", expect, shape.Area(), diff)
}
}
t.Run("rectangles", func(t *testing.T) {
rectangle := Rectangle{12, 6}
checkArea(t, rectangle, 72.0)
})
t.Run("circles", func(t *testing.T) {
circle := Circle{10, 0, 0}
checkArea(t, circle, 314.1592653589793)
})
}
go 之中接口解析是隐式的, 不需要显式 implements xxx(interface),只需要确实有这个方法,就能成功编译
Best Practice:表结构测试, 使用t.Run和测试表和#%v(打印结构)来获得清晰的测试
import (
"math"
"testing"
)
func TestArea(t *testing.T){
checkArea := func(t *testing.T, name string, shape Shape, expect float32){
t.Run(name, func(t *testing.T){
diff := shape.Area() - expect
if math.Abs(float64(diff)) > 1e-6{
t.Errorf("in %#v, expect %v, got %v, diff %v", shape, expect, shape.Area(), diff)
}
})
}
testTable:= []struct{
name string
shape Shape
expect float32
}{
{name: "Rectangle",shape: Rectangle{width: 10, height: 10}, expect: 100},
{name:"Circle", shape: Circle{radius: 10}, expect: 314.1592653589793},
}
for _, test :=range testTable{
checkArea(t, test.name, test.shape, test.expect)
}
}
指针和错误
go 的函数默认都是值传递,而指针又由于语法糖表现得和引用(的语法)差不多
func (w *Wallet) Withdraw(amount Bitcoin) {
w.balance -= amount
} // 这是this->balance -= amount
func (w Wallet) Withdraw(amount Bitcoin) {
w.balance -= amount
} // 这是w = (*this), w.balance -= amount
go 用返回值来表示一个函数可能抛出错误(其中返回值的类型为 error)
如果这个 error 是 nil(null 的变体),那就是正常,否则是错误,例如
func (w *Wallet) Withdraw(amount Bitcoin) error {
if amount > w.balance {
return errors.New("oh no")
}
w.balance -= amount
return nil
}
检测一个类是否实现了接口的全部方法:
var _ Interface = (*Class)(nil) // 如果Class类的指针不能强制转换成接口,就没有全部实现
空接口:可以表示任何类型
func main(){
m := make(map[string]interface{})
m["name"]="Tom"
m["age"]=18
m["parents"]=Persons{A, B}
}
map map[key]value
- 传递指针(所以不需要传递 map 的引用,直接传递就行)
var m map[string]string后,m 是 nil,这不好。这不是空 map
var dictionary = map[string]string{}
// OR
var dictionary = make(map[string]string)
泛型
type List[T any] struct {
head, tail *element[T]
}
type element[T any] struct {
next *element[T]
val T
}
func (lst *List[T]) Push(v T) {
if lst.tail == nil {
lst.head = &element[T]{val: v}
lst.tail = lst.head
} else {
lst.tail.next = &element[T]{val: v}
lst.tail = lst.tail.next
}
}
并发
- goroutine
package main
import (
"fmt"
"sync"
"time"
)
func ConcurrentDownload(url string) {
var wg sync.WaitGroup
download := func (name string) {
fmt.Printf("Start Downloading %v.\n", name)
time.Sleep(time.Second) // simulate
fmt.Printf("%v finished.\n", name)
wg.Done()
}
for i:=0;i<10;i++{
wg.Add(1)
go download("File"+fmt.Sprint(i))
}
wg.Wait()
}
func main(){
ConcurrentDownload("http://example.com")
}
其中,
sync.WaitGroup用于等待所有协程结束(其实是一个计数器,在计数为 0 的时候 Wait()就不堵塞了,Add()加一,Done()减一)go <dosomething>起一个 dosomething 的协程
- channel
并发协程的通信管道
package main
import (
"fmt"
)
func PrintFib(n int) {
fib_chan := make(chan int)
go func(){
x := 0
y := 1
for i:=0;i<n;i++{
fib_chan <- x
x, y = y, x + y
}
close(fib_chan)
}()
for num := range fib_chan{
fmt.Printf("%d ", num)
fmt.Println()
}
}
func main(){
PrintFib(10)
}
其中,
- make 是一个类似 malloc 的东西, 但他可以自动 gc,并且可以分配 channel 管道
<-是一个运算符, 对 chan 使用,代表把右边的丢进 chan- 用 make 创建 chan,可以有第二个参数,代表缓冲区大小
- 如果没有,无缓冲,只有当接受和发送端都就绪才会开始发送
- 如果有,size 为 n,意味着有一个 n 个元素大小的缓 冲区,发送端可以提前发送,缓冲区满之前不需要等待接收端
- chan 支持 range 语法,range 会沿着管道迭代直至关闭——管道只能被发送方关闭,接收方关闭管道后继续发送会 panic
用 channel 也可以进行这样的同步:
package main
import (
"fmt"
"time"
)
func worker(done chan bool) {
fmt.Print("working...")
time.Sleep(time.Second)
fmt.Println("done")
done <- true
}
func main() {
done := make(chan bool, 1)
go worker(done)
<-done // 在这里起到了类似wait()或者join()的效果
}
可以指定一个 channel 只可以接受/发送
func pong(pings <-chan string, pongs chan<- string) {
// pings只能发送,pongs只能接受
msg := <-pings
pongs <- msg
}
- select
可以同时等待多个 channel 上的消息
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
select 实现超时——一个协程干活,一个协程 Sleep timeout 秒,然后 select 两个协程
select 实现非堵塞管道——一个 default case,不接收东西
协程理解:
- 轻量级线程,轻量级体现在是在用户态的堆上去模拟栈结构,在代码之中自己调度,而不是通过 syscall 陷入内核调度——其中非抢占式的实现(例如 python 的 asyncio),通过 yield 等在阻塞时主动让出;抢占式的实现可以直接被调度器调度(例如 goroutine)
- 也因为是在用户态的调度,协程实际上没有并行的功能,只是提供了一个非阻塞的执行流,多个协程在同一个线程上执行,也只占据 cpu 的一个核心
- 协程的用处在于,以一个轻量级的方式去完成堵塞的功能(例如 IO, network),执行两个 IO 请求,cpu 工作 1ms,IO 工作 1s,如果是两个线程,cpu 的 1ms 可以并行,但受限于线程创建回收调度开销和系统最大线程资源限制 ,实际损耗的时间很可能大于 ms 级别。而协程在 cpu 上需要依次运行两个 1ms,但是同样不需要被 IO 堵塞
- 具体的实现方式而言,分为有栈协程和无栈协程,有栈协程有自己的一段模拟的栈空间,在切换时保存寄存器到栈里面;无栈协程使用状态机实现,在切换时记录状态机的状态。有栈协程开销更大,但表达力更强
- ts 里面的 async/await 就是协程的包装,await 就是记录状态的点,程序向下执行到 await 处暂停协程,等待 await 事件完成
timer&ticker 内置计时器, 可用于限流等
defer 延迟做某件事,常见于模拟一种 RAII,无论是何种方式退出都能正常执行
func writeToFile(filename string, data []byte) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close() // 确保文件在函数结束时被关闭,defer后的调用会在defer所在的函 数结束后执行
_, err = file.Write(data)
return err
}
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
worker(i)
}()
}
- 原子操作
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var ops atomic.Uint64
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
for c := 0; c < 1000; c++ {
ops.Add(1)
}
wg.Done()
}()
}
wg.Wait()
fmt.Println("ops:", ops.Load()) //50000
}
- mutex 锁
type Container struct {
mu sync.Mutex
counters int
}
func (c *Container) inc(name string) {
c.mu.Lock()
defer c.mu.Unlock()
c.counters++
}
Useful API
Sort 排序(slices) 第二个参数是谓词
自定义排序之中的内置类型的比较用cmp.Compare()
例如
PersonCmp := func(a *Person, b *Person){
return cmp.Compare(a.name, b.name)
}
slices.SortFunc(Persons, PersonCmp)
slices.Sort(ints)
slices.isSorted(ints)
go 有内置panic(msg string)和recover(在 defer 内部,catch panic)
// recover 的返回值是调用 panic 时引发的错误。
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered. Error:\n", r)
}
}()
mayPanic()
字符串相关函数
package main
import (
"fmt"
s "strings"
)
var p = fmt.Println
func main() {
p("Contains: ", s.Contains("test", "es"))
p("Count: ", s.Count("test", "t"))
p("HasPrefix: ", s.HasPrefix("test", "te"))
p("HasSuffix: ", s.HasSuffix("test", "st"))
p("Index: ", s.Index("test", "e"))
p("Join: ", s.Join([]string{"a", "b"}, "-"))
p("Repeat: ", s.Repeat("a", 5))
p("Replace: ", s.Replace("foo", "o", "0", -1))
p("Replace: ", s.Replace("foo", "o", "0", 1))
p("Split: ", s.Split("a-b-c-d-e", "-"))
p("ToLower: ", s.ToLower("TEST"))
p("ToUpper: ", s.ToUpper("test"))
}
正则:regexp包
JSON 包,rand 包
parse 数字,
package main
import (
"fmt"
"strconv"
)
func main() {
f, _ := strconv.ParseFloat("1.234", 64)
i, _ := strconv.ParseInt("123", 0, 64) // 0:从字符串推断基数, 64:int64
d, _ := strconv.ParseInt("0x1c8", 0, 64)
u, _ := strconv.ParseUint("789", 0, 64)
k, _ := strconv.Atoi("135")
_, e := strconv.Atoi("wat")
}
文件 IO,完全类似 c
ReadFile 读取整个文件,Read 读取几个字节(用make([]byte, size)装),Seek 有三种方式 io.SeekStart, io.SeekCurrent, io.SeekEnd,bufio 提供带缓冲 IO
Write, WriteFile, Create, Flush, Sync, WriteString(in bufio)同理
删除文件 os.Remove
文件路径path/filepath包,filepath.Join 连接路径 Ext 得到拓展名, Dir 得到文件目录,Base 得到文件名
文件夹 os.Mkdir, os.RemoveAll (等效于rm -rf), os.Chdir, os.ReadDir(得到文件的集合)
WalkDir 递归遍历
err = filepath.WalkDir("subdir", visit)
func visit(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Println(" ", path, d.IsDir())
return nil
}
//go:embed 在编译出的二进制文件之中包含
//go:embed folder/single_file.txt
var fileString string // 将folder/single_file.txt的内容放到fileString里面,并在编译时带上该txt
os.Args 命令行参数
os.Getenv / Setenv 环境变量
Gin
start
r 指定一个 wsgi 应用实例
GET 路由响应
Run() 可以带一个参数,例如Run(":9090")