golang 开发中知识积累(持更)

Golang 新手可能会踩的 50 个坑

目录

reflect 反射场景应用

动态调用函数(有参,无参,有返回值)

type T struct{}

func (t *T) Do() {
fmt.Println("Hello, world!")
}

func (t *T) DoWithPara(name string, index int) {
fmt.Println("Hello, ", name, index)
}

func (t *T) DoWithErr() (string, error) {
return "hello, world", errors.New("new error")
}

func DynamicCall() {

    name1 := "Do"
    name2 := "DoWithPara"
    name3 := "DoWithErr"

    t := new(T)

    reflect.ValueOf(t).MethodByName(name1).Call(nil)

    name := reflect.ValueOf("world")
    index := reflect.ValueOf(1234)
    in := []reflect.Value{name, index}
    reflect.ValueOf(t).MethodByName(name2).Call(in)

    ret := reflect.ValueOf(t).MethodByName(name3).Call(nil)
    fmt.Println(ret[0], ret[1].Interface().(error))
}

struct tag 解析

type B struct {
    A int    `json:"aaa" test:"aaatest"`
    B string `json:"bbb" test:"bbbtest"`
}

func ParseStruct() {
    t := B{
        A: 123456,
        B: "helloworld",
    }
    tt := reflect.TypeOf(t)

    for i := 0; i < tt.NumField(); i++ {
        field := tt.Field(i)
        if json, ok := field.Tag.Lookup("json"); ok {
            fmt.Println(json)
        }

        test := field.Tag.Get("test")
        fmt.Println(test)
    }
}

类型转换与赋值

type C struct {
    A int    `newC:"AA"`
    B string `newC:"BB"`
}

type NewC struct {
    AA int
    BB string
}

func transAndSet() {
    t := C{
        A: 111,
        B: "hello",
    }
    tt := reflect.TypeOf(t)
    tv := reflect.ValueOf(t)

    newC := new(NewC)
    newCv := reflect.ValueOf(newC)

    for i := 0; i < tt.NumField(); i++ {
        field := tt.Field(i)
        newCTag := field.Tag.Get("newC")

        tValue := tv.Field(i)
        newCv.Elem().FieldByName(newCTag).Set(tValue)
    }

    fmt.Println(*newC)
}

通过kind()处理不同分支

func differentSwitch() {
    a := 12345
    t := reflect.TypeOf(a)

    switch t.Kind() {
        case reflect.Int:
            fmt.Println("int")
        case reflect.String:
            fmt.Println("string")
        default:
            fmt.Println("Unknown")
    }
}

判断实例是否实现了某接口

type IT interface {
    test1()
}

type D struct {
    A string
}

func (a *D) test2() {
    fmt.Println("test1")
}

func isImp() {
    ITF := reflect.TypeOf((*IT)(nil)).Elem()

    t := new(D)
    tt := reflect.TypeOf(t)

    res := tt.Implements(ITF)

    fmt.Println(res, ITF)
}

匿名函数的坑

在没有将变量 v 的拷贝值传进匿名函数之前,只能获取最后一次循环的值

func closureRange(){
    //var a chan int
    s := []string{"a", "b", "c"}
    for _, v := range s {
        go func() {
            fmt.Println(v)
        }()
    }
    select {}    // 阻塞模式
}

//输出结果:
//c
//c
//c
//fatal error: all goroutines are asleep - deadlock!
func closureRange(){
    //var a chan int
    s := []string{"a", "b", "c"}
    for _, v := range s {
        go func(v string) {
            fmt.Println(v)
        }(v)
    }
    select {}    // 阻塞模式
}

//输出结果:
//c
//a
//b
//fatal error: all goroutines are asleep - deadlock!
//goroutine 1 [select &#40;no cases&#41;]:

每次 append 操作仅将匿名函数放入到列表中,但并未执行,并且引用的变量都是 i,随着 i 的改变匿名函数中的 i 也在改变,所以当执行这些函数时,他们读取的都是环境变量 i 最后一次的值。解决的方法就是每次复制变量 i 然后传到匿名函数中,让闭包的环境变量不相同。

func test() []func() {
    var s []func()

    for i := 0; i < 3; i++ {
        x := i                  //复制变量
        s = append(s, func() {
            fmt.Println("i:",&i, i)
            fmt.Println("x:",&x, x)
        })
    }

    return s
}

func ttt() {
    for _, f := range test() {
        f()
    }
}

//明显i的结果是不对的
//i: 0xc00001e098 3
//x: 0xc00001e0a0 0
//i: 0xc00001e098 3
//x: 0xc00001e0a8 1
//i: 0xc00001e098 3
//x: 0xc00001e0b0 2

GO46

1 Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量

channel


2 无缓冲Chan的发送和接收是否同步

同步


3 Golang并发机制以及它所使用的CSP并发模型

它底层是使用协程(coroutine)实现并发,coroutine是一种运行在用户态的用户线程 Golang的CSP并发模型,是通过Goroutine和Channel来实现的。


4 Golang中常用的并发模型

  1. 通过channel通知实现并发控制
  2. 通过sync包中的WaitGroup实现并发控制 Add,Done,Wait,
  3. Context上下文,实现并发控制

5 Go中对nil的Slice和空Slice的处理是一致的吗

不一致.

empty slice 是指slice不为nil,但是slice没有值,slice的底层的空间是空的


6 协程和线程和进程的区别

  • 进程是程序的一次执行过程,是程序在执行过程中的分配和管理资源的基本单位,每个进程都有自己的地址空间,进程是系统进行资源分配和调度的一个独立单位。
  • 线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源
  • 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。

7 Golang的内存模型中为什么小对象多了会造成GC压力

小对象过多会导致GC三色法消耗过多的CPU


8 Go中数据竞争问题怎么解决

互斥锁sync.Mutex或者也可以通过CAS无锁并发解决. 争检测机制,可以使用 go run -race 或者 go build -race来进行静态检测。


9 什么是channel,为什么它可以做到线程安全

发送一个数据到Channel和从Channel接收一个数据都是原子性的。设计Channel的主要目的就是在多任务间传递数据的,本身就是安全的。


10 Golang垃圾回收算法

标记清除垃圾回收算法

  • 栈扫描(开始时STW),所有对象最开始都是白色.
  • 从 root开始找到所有可达对象(所有可以找到的对象),标记为灰色,放入待处理队列。
  • 遍历灰色对象队列,将其引用对象标记为灰色放入待处理队列,自身标记为黑色。
  • 清除(并发) 循环步骤3直到灰色队列为空为止,此时所有引用对象都被标记为黑色,所有不可达的对象依然为白色,白色的就是需要进行回收的对象

11 GC的触发条件有哪些

主动触发(手动触发),通过调用runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕. 被动触发,分为两种方式:

  1. 使用系统监控,当超过两分钟没有产生任何GC时,强制触发 GC.
  2. 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,当前内存分配达到一定比例则触发.

12 Go的GPM模型如何调度的

P的数量可以通过GOMAXPROCS()来设置

  • M: M代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息.
  • G: 代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度.
  • P: P全称是Processor,逻辑处理器,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine.

img_28.png


13 并发编程的概念是什么

并发编程是指在一台处理器上“同时”处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。


14 Go语言的栈空间管理是怎么样的

动态地分配栈空间, 栈复制法(stack copying)。

会分配一个两倍大的内存块并把老的内存块内容复制到新的内存块里。这样做意味着当栈缩减回之前大小时,我们不需要做任何事情。栈的缩减没有任何代价。


15 Goroutine和Channel的作用分别是什么

协程,可以看作是轻量级的线程。但与线程不同的是,线程的切换是由操作系统控制的,而协程的切换则是由用户控制的。多个协程可以在多个处理器同时跑。

channel则是goroutinues之间进行通信的渠道。


16 怎么查看Goroutine的数量

runtime.NumGoroutine()


17 Go中的锁有哪些

  • 互斥锁 sync.Mutex
  • 读写锁 sync.RWMutex
  • sync.Map的安全的锁

18 怎么限制Goroutine的数量

func main(){
 ch = make(chan int,5) //控制并发
 wg := sync.WaitGroup{}
 for i:=0;i<10;i++{
  wg.Add(1)
  ch <-i //五个之后就会阻塞 除非有goroutine消费了chan 可以继续并发  但是上限是5
  go elegance(&wg)
 }
 wg.Wait()
 fmt.Println("end")
}

19 Channel是同步的还是异步的

异步的

Channel只由写方关闭,读方不可关闭。如果由读方关闭了,写方不知道继续写的话,就会引发panic

操作 一个零值nil通道 一个非零值但已关闭的通道 一个非零值且尚未关闭的通道
关闭 panic panic 成功关闭
永久阻塞 panic 阻塞或者成功发送
永久阻塞 永不阻塞 阻塞或者成功接收

20 Goroutine和线程的区别

  1. 从调度上看,goroutine的调度开销远远小于线程调度开销。
  2. 从栈空间上,goroutine的栈空间更加动态灵活。线程都有一个固定大小的栈内存,通常是2MB
  3. goroutine没有可供程序员访问的标识

21 Go的Struct能不能比较

相同struct类型的可以比较

不同struct类型的不可以比较,编译都不过,类型不匹配


22 Go的defer原理是什么

  1. 延迟函数的参数的值在defer语句出现时就已经确定下来了
  2. 延迟函数执行按后进先出顺序执行,即先出现的defer最后执行
  3. 延迟函数可以操作主函数的具名返回值

23 Go的select可以用于什么

select 机制是,监听多个channel,每一个 case 是一个事件,可以是读事件也可以是写事件,随机选择一个执行,可以设置default.


24 Context包的用途是什么

专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。

  • 不要把Context放在结构体中,要以参数的方式传递。
  • 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO。
  • Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递。
  • Context是线程安全的,可以放心的在多个goroutine中传递。

25 Go主协程如何等其余协程完再操作

sync.WaitGroup

func main() {

    var wg sync.WaitGroup

    wg.Add(2) // 因为有两个动作,所以增加2个计数
    go func() {
        fmt.Println("Goroutine 1")
        wg.Done() // 操作完成,减少一个计数
    }()

    go func() {
        fmt.Println("Goroutine 2")
        wg.Done() // 操作完成,减少一个计数
    }()

    wg.Wait() // 等待,直到计数为0
}

26 Go的Slice如何扩容

如果切片的容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;

一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一。

如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。


27 Go中的map如何实现顺序读取

可以先把map中的key,通过sort包排序.


package main

import (
    "fmt"
    "sort"
)

func main() {
    var m = map[string]int{
        "hello":         0,
        "morning":       1,
        "keke":          2,
        "jame":      3,
    }
    var keys []string
    for k := range m {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    for _, k := range keys {
        fmt.Println("Key:", k, "Value:", m[k])
    }
}

28 Go中CAS是怎么回事

CAS算法(Compare And Swap),是原子操作的一种, CAS算法是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization) go中CAS操作可以有效的减少使用锁所带来的开销,但是需要注意在高并发下这是使用cpu资源做交换的

package main

import (
 "fmt"
 "sync"
 "sync/atomic"
)

var (
 counter int32          //计数器
 wg      sync.WaitGroup //信号量
)

func main() {
 threadNum := 5
 wg.Add(threadNum)
 for i := 0; i < threadNum; i++ {
  go incCounter(i)
 }
 wg.Wait()
}

func incCounter(index int) {
 defer wg.Done()

 spinNum := 0
 for {
  // 原子操作
  old := counter
  ok := atomic.CompareAndSwapInt32(&counter, old, old+1)
  if ok {
   break
  } else {
   spinNum++
  }
 }
 fmt.Printf("thread,%d,spinnum,%d\n", index, spinNum)
}

29 Go中的逃逸分析是什么 30 Go值接收者和指针接收者的区别

如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。

通常我们使用指针作为方法的接收者的理由:

  1. 使用指针方法能够修改接收者指向的值。
  2. 可以避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。

31 Go的对象在内存中是怎样分配的

Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。


32 栈的内存是怎么分配的 33 堆内存管理怎么分配的

35 在Go函数中为什么会发生内存泄露

内存泄漏,指的是能够预期的能很快被释放的内存由于附着在了长期存活的内存上、或生命期意外地被延长,导致预计能够立即回收的内存而长时间得不到回收

  1. 当有一个全局对象时,可能不经意间将某个变量附着在其上,且忽略的将其进行释放,则该内存永远不会得到释放。
  2. 如果一个程序持续不断地产生新的 goroutine、且不结束已经创建的 goroutine 并复用这部分内存,就会造成内存泄漏的现象.

36 Go中new和make的区别

值类型:int,float,bool,string,struct和array.

变量直接存储值,分配栈区的内存空间,这些变量所占据的空间在函数被调用完后会自动释放。

引用类型:slice,map,chan和值类型对应的指针.

变量存储的是一个地址(或者理解为指针),指针指向内存中真正存储数据的首地址。内存通常在堆上分配,通过GC回收。

  • new该方法的参数要求传入一个类型,而不是一个值,它会申请一个该类型大小的内存空间,并会初始化为对应的零值,返回指向该内存空间的一个指针
  • make也是用于内存分配,但是和new不同,只用来引用对象slice、map和channel的内存创建,它返回的类型就是类型本身,而不是它们的指针类型。

37 G0的作用

g0 作为一个特殊的 goroutine,为 scheduler 执行调度循环提供了场地(栈)。对于一个线程来说,g0 总是它第一个创建的 goroutine。之后,它会不断地寻找其他普通的 goroutine 来执行,直到进程退出。

当需要执行一些任务,且不想扩栈时,就可以用到 g0 了,因为 g0 的栈比较大。g0 其他的一些“职责”有:创建 goroutine、deferproc 函数里新建 _defer、垃圾回收相关的工作(例如 stw、扫描 goroutine 的执行栈、一些标识清扫的工作、栈增长)等等。

因为 g0 这样一个特殊的 goroutine 所做的工作,使得 Go 程序运行地更快。


41 Go中的http包的实现原理

42 Goroutine发生了泄漏如何检测

通过Go自带的工具pprof或者使用Gops去检测诊断当前在系统上运行的Go进程的占用的资源


43 Go函数返回局部变量的指针是否安全

在 Go 中是安全的,Go 编译器将会对每个局部变量进行逃逸分析。如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上.


44 Go中两个Nil可能不相等吗

Go中两个Nil可能不相等。

  • 两个接口值比较时,会先比较 T,再比较 V。
  • 接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。

46 为何GPM调度要有P

  1. 每个 P 有自己的本地队列,大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少。
  2. 每个 P 相对的平衡上,在 GMP 模型中也实现了 Work Stealing 算法,如果 P 的本地队列为空,则会从全局队列或其他 P 的本地队列中窃取可运行的 G 来运行,减少空转,提高了资源利用率。

一般来讲,M 的数量都会多于 P。像在 Go 中,M 的数量默认是10000,P 的默认数量的 CPU 核数。另外由于 M 的属性,也就是如果存在系统阻塞调用,阻塞了 M,又不够用的情况下,M 会不断增加。

M 不断增加的话,如果本地队列挂载在 M 上,那就意味着本地队列也会随之增加。这显然是不合理的,因为本地队列的管理会变得复杂,且 Work Stealing 性能会大幅度下降。

M 被系统调用阻塞后,我们是期望把他既有未执行的任务分配给其他继续运行的,而不是一阻塞就导致全部停止。

因此使用 M 是不合理的,那么引入新的组件 P,把本地队列关联到 P 上,就能很好的解决这个问题。


recover

Recover是一个从panic恢复的内建函数。Recover只有在defer的函数里面才能发挥真正的作用。如果是正常的情况(没有发生panic),调用recover将会返回nil并且没有任何影响。如果当前的goroutine panic了,recover的调用将会捕获到panic的值,并且恢复正常执行。

defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()

defer的规则

参考资料

  • 规则一:延迟函数的参数在defer语句出现时就已经确定下来了
  • 规则二:延迟函数执行按后进先出顺序执行,即先出现的defer最后执行
  • 规则三:延迟函数可能操作主函数的具名返回值
  • 返回规则一:主函数拥有匿名返回值,返回字面值,无法影响返回值。
  • 返回规则二:主函数拥有匿名返回值,返回变量,对函数返回值造成影响。
  • 返回规则三:主函数拥有具名返回值如果defer语句操作该返回值,可能改变返回结果。

init 函数和 main 函数的异同

相同点: 两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。

不同点: init函数可以应用于任意包中,且可以重复定义多个。 main函数只能用于main包中,且只能定义一个。


常用包:path
  • path.Split("static/myfile.css") 返回文件夹和文件名
  • path.Match("a*", "abc") 正则匹配
  • path.IsAbs("/dev/null") 是否绝对路径
  • path.Ext("/a/b/c/bar.css") 文件后缀
  • path.Dir("/a/b/c") 上级文件夹
  • path.Base("/a/b") 返回路径的最后一个元素

函数的一致判定

只要两个函数的参数列表和结果列表中的元素顺序及其类型是一致的,我们就可以说它们是一样的函数,或者说是实现了同一个函数类型的函数。

byte, rune

byte是uint8的别名类型,而rune是int32的别名类型。

Go语言的字符有以下两种: 一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。 另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。

空的花括号

一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。

{}还可以用来表示其值不包含任何元素,比如空的切片值[]string{},以及空的字典值map[int]string{}

struct{},它就代表了不包含任何字段和方法的、空的结构体类型。 interface{} 则代表了不包含任何方法定义的、空的接口类型


string 类型的值是常量,不可更改

尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许的。

string 类型的值是只读的二进制 byte slice,如果真要修改字符串中的字符,将 string 转为 []byte 修改后,再转为 string 即可


字符串的长度

Go 的内建函数 len() 返回的是字符串的 byte 数量,而不是像 Python 中那样是计算 Unicode 字符数。

如果要得到字符串的字符数,可使用 "unicode/utf8" 包中的 RuneCountInString(str string) (n int)

func main() {
    char := "♥"
    fmt.Println(utf8.RuneCountInString(char))    // 1
}

注意: RuneCountInString 并不总是返回我们看到的字符数,因为有的字符会占用 2 个 rune:


自增和自减运算

go没有 --i ++i 只有 i++ i--


关闭 HTTP 连接、响应体

先检查 HTTP 响应错误为 nil,再调用 resp.Body.Close() 来关闭响应体

一些支持 HTTP1.1 或 HTTP1.0 配置了 connection: keep-alive 选项的服务器会保持一段时间的长连接。但标准库 "net/http" 的连接默认只在服务器主动要求关闭时才断开,所以你的程序可能会消耗完 socket 描述符。

根据需求选择使用场景:

  • 若你的程序要向同一服务器发大量请求,使用默认的保持长连接。
  • 若你的程序要连接大量的服务器,且每台服务器只请求一两次,那收到请求后直接关闭连接。或增加最大文件打开数 fs.file-max 的值。

recover的使用

在一个 defer 延迟执行的函数中调用 recover() ,它便能捕捉 / 中断 panic

// 错误的 recover 调用示例
func main() {
    recover()    // 什么都不会捕捉
    panic("not good")    // 发生 panic,主程序退出
    recover()    // 不会被执行
    println("ok")
}

// 正确的 recover 调用示例
func main() {
    defer func() {
        fmt.Println("recovered: ", recover())
    }()
    panic("not good")
}

断言的失败导致异常

断言失败则会返回目标类型的“零值”,断言变量与原来变量混用可能出现异常情况

 var data interface{} = "great"

 if data, ok := data.(int); ok {} else {  fmt.Println("data: ", data) } // 0  断言失败则会返回目标类型的“零值” 

 if res, ok := data.(int); ok {} else {  fmt.Println("data: ", data) }  // great 未混用变量,正常输出原有值

json.RawMessage

可以使用 struct 将数值类型映射为 json.RawMessage 原生数据类型 适用于JSON 某个字段的值类型不固定等情况

type TestStruct struct {

    Type int
    Body json.RawMessage
}

type Person struct {
    Name string
    Age int
}

type Worker struct {

    Name string
    Job string
}

func main(){
    input := `
       {
        "Type": 1,
        "Body":{ 
            "Name":"ff",
            "Age" : 19
         }
    }`

    ts := TestStruct{}

    if  err := json.Unmarshal([]byte(input), &ts); err!= nil {
        panic( err)
    }

    switch  ts.Type {
    case 1:
        var p Person
        if err := json.Unmarshal(ts.Body, &p); err != nil {
            panic(err)
        }
        fmt.Println(p)
    case 2:
        var w Worker
        if err := json.Unmarshal(ts.Body, &w); err != nil {
            panic(err)
        }
        fmt.Println(w)
    }

}
函数返回值是匿名函数

对于闭包而言,其初始化一次后,匿名函数内的变量就一直不被销毁(代码区域内),每次执行闭包,闭包内的变量有保存了上一次运行后的值。

package main

import "fmt"

func squares(a int) func() int {
   var x int
   x = a
   fmt.Println("函数初始 x=:", x)
   return func () int {
      fmt.Println("匿名函数 x=:", x)
      x += 2
      return x*x
   }
}

func main() {

   f := squares(1)
   fmt.Println(f())
   fmt.Println(f())
}

输出结果如下:

函数初始 x=: 1
匿名函数 x=: 1
9
匿名函数 x=: 3
25

无序的map,有序的json

无序的map

package main

import "fmt"

func main() {
    A := make(map[int]string)
    letter := []string{"a","b","c","d","e","f","g","h"}
    for k,v := range letter{
        A[k] = v
    }
    fmt.Println(A) //注意,字典是无序的哟!
}

#以上代码执行结果如下:
map[7:h 0:a 1:b 2:c 3:d 4:e 5:f 6:g]

map是无序的,每次取出key/value的顺序都可能不一致,但 map转json是有序的,按照ASCII码升序排列key。


map
var b map[int]int
var c = map[int]int{}
d := map[int][]int{}

DD(b, c, d)

//DD输出结果如下

type: map[int]int
#val: map[int]int(nil)
-val: map[]
===============================

type: map[int]int
#val: map[int]int{}
-val: map[]
===============================

type: map[int][]int
#val: map[int][]int{}
-val: map[]

//以下语法存在问题 使用了= 那么就是赋值,就得是完整的值
//报错如下 type map[int]int is not an expression
//var e = map[int]int
var (
    li = []*model.CoolPower{
        {Name: "动画", Num: 0},
        {Name: "艺术", Num: 0},
        {Name: "游戏", Num: 0},
        {Name: "科技", Num: 0},
        {Name: "影视", Num: 0},
        99 : {Name: "鬼畜", Num: 0},
    }
)

DD(li[99])

//输出

type: *model.CoolPower
#val: &model.CoolPower{Num:0, Name:"鬼畜"}
-val: &{0 鬼畜}

// ExtraQst etc.
type ExtraQst struct {
    ID       int64     `json:"id"`
    Question string    `json:"question"`
    Ans      int8      `json:"ans"`
    Status   int8      `json:"status"`
    OriginID int64     `json:"origin_id"`
    AvID     int64     `json:"av_id"`
    Source   int8      `json:"source"`
    Ctime    time.Time `json:"ctime"`
    Mtime    time.Time `json:"mtime"`
}

qmap := map[int8][]int64{}
for _, q := range qs {
    qmap[q.TypeID] = append(qmap[q.TypeID], q.ID)
}

数组append切片添加
a := []int{1, 2, 3}
a = append(a, 1)            // 追加1个元素
a = append(a, 1, 2, 3)      // 追加多个元素, 手写解包方式
a = append(a,[]int{11,11,11}...)    // 追加一个切片, 切片需要解包
a = append([]int{22,22,22},a...)    // 在开头添加1个切片
a = append(a[:0], append([]int{1,2,3}, a[0:]...)...) // 在第0个位置插入切片
fmt.Println(a)
for i:=0;i<len(a);i++{
    fmt.Println(a[i])
}

输出:

[1 2 3 22 22 22 1 2 3 1 1 2 3 11 11 11]
1
2
3
22
22
22
1
2
3
1
1
2
3
11
11
11

判断map中的key是否存在
a := make(map[string]string)

a["test"] = "testing"

if v, ok := a["test"]; ok {
    fmt.Println(ok)
    fmt.Println(v)
}

输出:

true

testing


常用的tag
  • json json序列化或反序列化时字段的名称
  • db sqlx模块中对应的数据库字段名
  • form gin框架中对应的前端的数据字段名
  • binding 搭配 form 使用, 默认如果没查找到结构体中的某个字段则不报错值为空, binding为 required 代表没找到返回错误给前端

string []byte

string是一系列8位字节的集合,通常但不一定代表UTF-8编码的文本。字符串可以为空,但不能为nil。

string就是一系列字节,而[]byte也可以表达一系列字节,那么实际运用中应当如何取舍?

  • string可以直接比较,而[]byte不可以,所以[]byte不可以当map的key值。
  • 因为无法修改string中的某个字符,需要粒度小到操作一个字符时,用[]byte。
  • string值不可为nil,所以如果你想要通过返回nil表达额外的含义,就用[]byte。
  • []byte切片这么灵活,想要用切片的特性就用[]byte。
  • 需要大量字符串处理的时候用[]byte,性能好很多。

string转为[]byte,语法[]byte(string)[]byte转为string,语法string([]byte)

string不能直接和byte数组转换, string可以和byte的切片转换

//string 转为[]byte
var str string = "test"

var data1 []byte = []byte(str)

fmt.Println(data1)

//byte转为string
var data [10]byte

data[0] = 'T'

data[1] = 'E'

str = string(data[:])

fmt.Println(str)

输出结果如下:

[116 101 115 116]
TE        


range 中的注意点
type student struct {
    Name string
    Age  int
}

func main() {
    var students []student

    students = []student{
        {Name: "one", Age: 18},
        {Name: "two", Age: 19},
    }

    data := make(map[int]*student)

    // code1 这是不对的使用方法
    for i, v := range students {
        data[i] = &v
    }

    fmt.Println(data)

    // code2 这是正确的使用方法
    for i, _ := range students {
        data[i] = &students[i]
    }
    fmt.Println(data)

}

输出值如下,说明range之后的v值是值拷贝,data[i] = &v 永远取的值都是v这个遍历的地址

map[0:0xc000004460 1:0xc000004460]
map[0:0xc000060360 1:0xc000060378]

mutex 互斥锁
  1. 互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界区代码
  2. Lock()和Unlock()定义临界区
var (
    //全局变量
    counter int64
    //计数信号量
    wg sync.WaitGroup
    //mutex定义一段代码临界区
    mutex sync.Mutex
)

func main() {
    fmt.Println("hello")
    //计数加2,等待两个goroutine
    wg.Add(2)
    go incCounter()
    go incCounter()
    //主goroutine等待子goroutine结束
    wg.Wait()
    fmt.Println("最终counter值:", counter)
}

//增加counter的值函数
func incCounter() {
    //函数结束,减小信号量
    defer wg.Done()
    for count := 0; count < 2; count++ {
        //创建这个临界区
        //同一时刻只允许一个goroutine进入
        mutex.Lock()
        //使用大括号只是为了让临界区看起来更清晰,并不是必须的
        {
            fmt.Println("count" + strconv.Itoa(count))
            value := counter
            fmt.Println("counter 01:" + strconv.Itoa(int(counter)))

            ////强制调度器切换
            runtime.Gosched()
            value++
            counter = value
            fmt.Println("counter 02:" + strconv.Itoa(int(counter)))
        }
        mutex.Unlock()
    }
}

开启互斥锁,运行结果如下:

同一时刻只允许一个goroutine进入

hello
count0
counter 01:0
counter 02:1
count1
counter 01:1
counter 02:2
count0
counter 01:2
counter 02:3
count1
counter 01:3
counter 02:4
最终counter值: 4

关闭互斥锁,运行结果如下:

同一时刻有多个goroutine进入

hello
count0
counter 01:0
count0
counter 01:0
counter 02:1
count1
counter 01:1
counter 02:2
counter 02:1
count1
counter 01:1
counter 02:2
最终counter值: 2

Once 对象

Once 是一个可以被多次调用但是只执行一次,若每次调用Do时传入参数f不同,但是只有第一个才会被执行。

func (o *Once) Do(f func())


    var once sync.Once
    onceBody := func() {
        fmt.Println("Only once")
    }
    done := make(chan bool)
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(onceBody)
            done <- true
        }()
    }
    for i := 0; i < 10; i++ {
        <-done
    }

如果你执行这段代码会发现,虽然调用了10次,但是只执行了1次。BTW:这个东西可以用来写单例。


WaitGroup
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()

wait group 用来等待一组goroutines的结束,

  1. 在主Goroutine里声明,
  2. 并且设置要等待的goroutine的个数,
  3. 每个goroutine执行完成之后调用 Done,
  4. 最后在主Goroutines 里Wait即可。

下面是个官方的例子:

var wg sync.WaitGroup
var urls = []string{
        "http://www.golang.org/",
        "http://www.google.com/",
        "http://www.somestupidname.com/",
}
for _, url := range urls {
        // Increment the WaitGroup counter.
        wg.Add(1)
        // Launch a goroutine to fetch the URL.
        go func(url string) {
                // Decrement the counter when the goroutine completes.
                defer wg.Done()
                // Fetch the URL.
                http.Get(url)
        }(url)
}
// Wait for all HTTP fetches to complete.
wg.Wait()

强制类型转换

这时候需要强制类型转换

package main

import "fmt"

func main() {
    var a float32 = 5.6
    var b int = 10
    fmt.Println (a * float32(b))
}

这样就不会报错了 普通变量类型int,float,string 都可以使用 type (a) 这种形式来进行强制类型转换

指针的强制类型转换需要用到unsafe包中的函数实现

package main

import "unsafe"
import "fmt"

func main() {
    var a int =10
    var b *int =&a
    var c *int64 = (*int64)(unsafe.Pointer(b))
    fmt.Println(*c)
}

断言

类型断言还有一种用法

类型断言表达式的语法形式是x.(T)。其中的x代表要被判断类型的值。这个值当下的类型必须是接口类型的,不过具体是哪个接口类型其实是无所谓的。

container := map[int]string{0: "zero", 1: "one", 2: "two"}

value, ok := interface{}(container).([]string)
package main

import "fmt"

func main() {
    var a interface{} =10
    t,ok:= a.(int)
    if ok{
        fmt.Println("int",t)
    }
    t2,ok:= a.(float32)
    if ok{
        fmt.Println("float32",t2)
    }
}

t,ok:= a.(int)有两个返回值,第一个是对应类型的值,第二个是bool类型的,类型判断是否正确

断言只是预知了值的类型,并不是类型转换


多值返回
func test() (res1 string,res2 string) {
    res1 = "2"
    res2 = "3"
    return
}

func main() {
    DD(test())
}

以上没有明确定义返回 return res1, res2 但效果是一样的。DD 打印出的结果如下:

type: string
#val: "2"
-val: 2
===============================

type: string
#val: "3"
-val: 3
===============================

defer 和 return

return之前的defer才会执行,return后面的defer是不会执行的

切片传递

切片的容量/长度/值


s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
s4 := s3[3:6]
fmt.Printf("The length of s4: %d\n", len(s4))
fmt.Printf("The capacity of s4: %d\n", cap(s4))
fmt.Printf("The value of s4: %d\n", s4)

The length of s4: 3
The capacity of s4: 5
底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾。
所以,s4的容量就是其底层数组的长度8, 减去上述切片表达式中的那个起始索引3,即5。
The value of s4: [4 5 6]

切片传递

我们可以认为,切片在内部可由一个结构体类型表示。这是它的表现形式,

type slice struct {  
    Length        int
    Capacity      int
    ZerothElement *byte
}

切片包含长度、容量和指向数组第零个元素的指针。 当切片传递给函数时,即使它通过值传递,指针变量也将引用相同的底层数组。 因此,当切片作为参数传递给函数时,函数内所做的更改也会在函数外可见。让我们写一个程序来检查这点。

package main

import (
    "fmt"
)

func subtactOne(numbers []int) {
    for i := range numbers {
        numbers[i] -= 2
    }
}
func main() {
    nos := []int{8, 7, 6}
    fmt.Println("slice before function call", nos)
    subtactOne(nos)                               // function modifies the slice
    fmt.Println("slice after function call", nos) // modifications are visible outside
}

输出: slice before function call [8 7 6] slice after function call [6 5 4]

switch

  • case执行完成后自动跳出,不用像PHP一样要加break
  • default 默认情况,与PHP相同
  • fallthrough 执行完当前case继续执行下一个case
  • case可以为多条件判断,隔开

for循环

break语句用于在完成正常执行之前突然终止 for 循环

func main() {  
    for i := 1; i <= 10; i++ {
        if i > 5 {
            break //loop is terminated if i > 5
        }
        fmt.Printf("%d ", i)
    }
    fmt.Printf("\nline after for loop")
}

continue 语句用来跳出 for 循环中当前循环。循环体会在一下次循环中继续执行。

if语句

if 还有另外一种形式,它包含一个 statement 可选语句部分,该组件在条件判断之前运行。它的语法是

if statement; condition {  
}

默认值

  • 整形如int8、byte、int16、uint、uintprt等,默认值为0
  • 浮点类型如float32、float64,默认值为0
  • 布尔类型bool的默认值为false
  • 复数类型如complex64、complex128,默认值为0+0i
  • 字符串string的默认值为""
  • 错误类型error的默认值为nil
  • 对于一些复合类型,如指针、切片、字典、通道、接口,默认值为nil
  • 数组的默认值要根据其数据类型来确定。例如:var a [4]int,其默认值为[0 0 0 0]。
time包

关于2006-01-02 15:04:05这里的123456的定义如下:

  • 月  1,01,Jan,January
  • 日  2,02,_2
  • 时  3,03,15,PM,pm,AM,am
  • 分  4,04
  • 秒  5,05
  • 年  06,2006
  • 时区 -07,-0700,Z0700,Z07:00,-07:00,MST
  • 周几 Mon,Monday

timeObj, err := time.Parse("layout:2006-01-02 15:04:05", "string:2019-08-29")

timeObj.Format("layout:20060102")

//几分钟前
time.Now().Add(time.Minute * -1)

//几天前
time.Now().AddDate(0, 0, -2)

//几月前
time.Now().AddDate(0, -3, 0)

//格式化
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, -3).Format("ytc_log_20060102")
关于初始化结构体

自定义一个结构体

type Rect struct {

    x, y float64

    width, height float64

}

初始化方法:

rect1 := new(Rect)

rect2 := &Rect{}

rect3 := &Rect{0, 0, 100, 200}

rect4 := &Rect{width:100, height:200}

注意这几个变量全部为指向Rect结构的指针(指针变量),因为使用了new()函数和&操作符.

而如果使用方法 a := Rect{} 则表示这个是一个Rect{}类型.两者是不一样的.

参考代码如下:

func main() {

    a := Rect{}
    a.x = 15

    rect1 := &Rect{0, 0, 100, 200}
    rect1.x = 10

    fmt.Printf("%v\n%T\n", a, a)
    fmt.Printf("%v\n%T\n", rect1, rect1)

}

运行结果:

{15 0 0 0}

main.Rect

&{10 0 100 200}

*main.Rect

在Go语言中,未进行初始化的变量都会被初始化为该类型的零值,例如:

  • bool类型的零值为false
  • int类型的零值为0
  • string类型的零值为空字符串.

在Go语言中没有构造函数的概念,对象的创建通常交由一个全局的创建函数来完成,以NewXXX来命令,表示"构造函数":

func NewRect(x ,y ,width, height float64) *Rect {

    return &Rect{x, y, width, height}

}

务必记得 make 仅适用于 map,slice 和 channel,并且返回的不是指针。应当用 new获得特定的指针。


重试方法
func init() {
    rand.Seed(time.Now().UnixNano())
}

func Retry(attempts int, sleep time.Duration, f func() error) error {
    if err := f(); err != nil {
        if s, ok := err.(stop); ok {
            // Return the original error for later checking
            return s.error
        }

        if attempts--; attempts > 0 {
            // Add some randomness to prevent creating a Thundering Herd
            jitter := time.Duration(rand.Int63n(int64(sleep)))
            sleep = sleep + jitter/2

            time.Sleep(sleep)
            return Retry(attempts, 2*sleep, f)
        }
        return err
    }

    return nil
}

type stop struct {
    error
}

cnt := 0
err := fmt.Errorf("test error every time")
a := func() error {
    if cnt == 0 {
        cnt++
        return err
    } else {
        cnt++
        return nil
    }
}
errFn := util.Retry(3, 1*time.Millisecond, a)

字符串转换

string转成int:

int, err := strconv.Atoi(string)

string转成int64:

int64, err := strconv.ParseInt(string, 10, 64)

int转成string:

string := strconv.Itoa(int)

int64转成string:

string := strconv.FormatInt(int64,10)


文件名 文件路径
files := "E:\\data\\test.txt"

paths, fileName := filepath.Split(files)

//获取路径中的目录及文件名 E:\data\  test.txt
fmt.Println(paths, fileName)  

//获取路径中的文件名test.txt    
fmt.Println(filepath.Base(files))

//获取路径中的文件的后缀 .txt 
fmt.Println(filepath.Ext(files))      
字符串截取

res := "http://12.95.10.47:7480/abc_cloud/20190731/12/1392015c2ad92ff3a1239f34efbf9a7d6d0fa7ac.png"

//结果:/20190731/12/1392015c2ad92ff3a1239f34efbf9a7d6d0fa7ac.png
fmt.println(res[33:len(res)]) 
Url解析
func main() {

//我们将解析这个 URL 示例,它包含了一个 scheme,认证信息,主机名,端口,路径,查询参数和片段。
    s := "postgres://user:pass@host.com:5432/path?k=v#f"

//解析这个 URL 并确保解析没有出错。
    u, err := url.Parse(s)
    if err != nil {
        panic(err)
    }

//直接访问 scheme。
    fmt.Println(u.Scheme)

//User 包含了所有的认证信息,这里调用 Username和 Password 来获取独立值。
    fmt.Println(u.User)
    fmt.Println(u.User.Username())
    p, _ := u.User.Password()
    fmt.Println(p)

//Host 同时包括主机名和端口信息,如过端口存在的话,使用 strings.Split() 从 Host 中手动提取端口。
    fmt.Println(u.Host)
    h := strings.Split(u.Host, ":")
    fmt.Println(h[0])
    fmt.Println(h[1])

//这里我们提出路径和查询片段信息。
    fmt.Println(u.Path)
    fmt.Println(u.Fragment)

//要得到字符串中的 k=v 这种格式的查询参数,可以使用 RawQuery 函数。你也可以将查询参数解析为一个map。已解析的查询参数 map 以查询字符串为键,对应值字符串切片为值,所以如何只想得到一个键对应的第一个值,将索引位置设置为 [0] 就行了。
    fmt.Println(u.RawQuery)
    m, _ := url.ParseQuery(u.RawQuery)
    fmt.Println(m)
    fmt.Println(m["k"][0])
}
//运行我们的 URL 解析程序,显示全部我们提取的 URL 的不同数据块。
$ go run url-parsing.go 
postgres
user:pass
user
pass
host.com:5432
host.com
5432
/path
f
k=v
map[k:[v]]
v

字符串的替换
    str := "welcome to outofmemory.cn"
    str = strings.Replace(str, " ", ",", -1)

    //welcome,to,outofmemory.cn
    fmt.Println(str)

DD函数

func DD(v ...interface{}) {
    for _, val := range v {
        fmt.Printf("┏-------------------------┓\ntype: %T\nval: %#v\naddress: %p\n┗-------------------------┛\n", val, val, val)
    }

    os.Exit(0)
}