切片 == 列表 map == 字典类型 线程不安全 要变成安全的就需要加锁 具体set.go 太麻烦 提供了更加简便的方法 互斥锁 HcMutex.Lock() HcMutex.UnLock() 同一时间只能一个执行 获取到锁 读写锁 RLock() unRLock() 写锁 Lock() UnLock()
package main
import (
"fmt"
"sync"
"time"
)
// 为了解决map线程不安全 ,我们自己加锁
type concurrentMap struct {
mp map[int]int
sync.RWMutex
}
// 通过set 方法做原有map的赋值 m[key] =v
func (c *concurrentMap) Set(key, value int) {
// 加写锁
c.Lock()
c.mp[key] = value
c.Unlock()
}
// 通过get 方法做原有map的读取值操作 v:= m[key]
func (c *concurrentMap) Get(key int) int {
//先获取读锁
c.RLock()
res := c.mp[key]
c.RUnlock()
return res
}
func main() {
c := concurrentMap{
mp: make(map[int]int),
}
// 一个线程循环写map
go func() {
for i := 0; i < 10000; i++ {
c.Set(i, i)
}
}()
// 一个线程循环读map
go func() {
for i := 0; i < 10000; i++ {
res := c.Get(i)
fmt.Printf("[cmap.get][%d=%d]\n", i, res)
}
}()
time.Sleep(1 * time.Hour)
}
m := sync.map{}
package main
import (
"fmt"
"log"
"strings"
"sync"
)
func main() {
m := sync.Map{}
// 新增
for i := 0; i < 10; i++ {
key := fmt.Sprintf("key_%d", i)
m.Store(key, i)
}
// 删除
m.Delete("key_8")
// 改m.Store
m.Store("key_9", 999)
// 查询
res, loaded := m.Load("key_09")
if loaded {
// 类型断言 res.(int)
log.Printf("[key_09存在 :%v 数字类型:%d]", res, res.(int))
}
// 遍历 return false 停止
m.Range(func(key, value interface{}) bool {
k := key.(string)
v := value.(int)
if strings.HasSuffix(k, "3") {
log.Printf("不想要3")
//return true
return false
} else {
log.Printf("[sync.map.Range][遍历][key:=%s][v:=%d]", k, v)
return true
}
})
// LoadAndDelete 先获取值再删掉
s1, loaded := m.LoadAndDelete("key_7")
log.Printf("key_7 LoadAndDelete :%v", s1)
s2, loaded := m.Load("key_7")
log.Printf("key_7 LoadAndDelete:%v", s2)
actual, loaded := m.LoadOrStore("key_8", 158)
if loaded {
log.Printf("key_8原来的值是:%v", actual)
} else {
log.Printf("key_8原来没有,实际是:%v", actual)
}
actual, loaded = m.LoadOrStore("key_1", 158)
if loaded {
log.Printf("key_1原来的值是:%v", actual)
} else {
log.Printf("key_1原来没有,实际是:%v", actual)
}
}
PS:golang 连接跳转到其他文章,Markdown的方式引用跳转 使用markdown语法:点击跳转
- 分片,多分片性能更高,互不影响 每个走自己的锁,减少加锁时间
package main
import (
"fmt"
"github.com/orcaman/concurrent-map"
"log"
"time"
)
func main() {
// Create a new map.
m := cmap.New()
// 循环写map
go func() {
for i := 0; i < 10000; i++ {
key := fmt.Sprintf("key_%d", i)
m.Set(key, i)
}
}()
// 循环读map
go func() {
for i := 0; i < 10000; i++ {
key := fmt.Sprintf("key_%d", i)
v, exists := m.Get(key)
if exists {
log.Printf("[%s=%v]", key, v)
}
}
}()
// 循环写map
go func() {
for i := 0; i < 10000; i++ {
key := fmt.Sprintf("key_%d", i)
m.Set(key, i)
}
}()
// 循环写map
go func() {
for i := 0; i < 10000; i++ {
key := fmt.Sprintf("key_%d", i)
m.Set(key, i)
}
}()
// 循环写map
go func() {
for i := 0; i < 10000; i++ {
key := fmt.Sprintf("key_%d", i)
m.Set(key, i)
}
}()
time.Sleep(1 * time.Hour)
}
带过期时间的缓存map
package main
import (
"fmt"
"log"
"sync"
"time"
)
//带过期时间的map 定时清理
type Cache struct {
sync.RWMutex
mp map[string]*item
}
type item struct {
value int //值
ts int64 // 时间戳,item 被创建出来的时间
}
func (c *Cache) Get(key string) *item {
c.RLock()
defer c.RUnlock()
return c.mp[key]
}
func (c *Cache) CacheNum() int {
c.RLock()
keys := make([]string, 0)
//i := 0
for k, _ := range c.mp {
//fmt.Println(k)
keys = append(keys, k)
//i++
}
c.RUnlock()
return len(keys)
}
func (c *Cache) Set(key string, value *item) {
c.Lock()
defer c.Unlock()
c.mp[key] = value
}
func (c *Cache) Clean(timeDelta int64) {
// 每5秒执行一此清理
for {
now := time.Now().Unix()
// 待删除的key的切片
toDelKeys := make([]string, 0)
// 先加读锁,把所有待删除的拿到
c.RLock()
for k, v := range c.mp {
// 时间比较
if now-v.ts > timeDelta {
// 认为这个k,v过期了,
// 不直接删除,为了降低加锁时间,加入待删除的切片
toDelKeys = append(toDelKeys, k)
}
}
c.RUnlock()
// 加写锁 删除,降低加写锁的时间
c.Lock()
for _, k := range toDelKeys {
log.Printf("[删除过期数据][key:%s]", k)
delete(c.mp, k)
}
c.Unlock()
// 写锁释放
time.Sleep(2 * time.Second)
}
}
func main() {
c := Cache{
mp: make(map[string]*item),
}
// 让清理的任务异步执行
// 每5秒运行一次,检查时间差大于30秒item 就删除
go c.Clean(30)
// 从mysql中读取到了数据,塞入缓存
for i := 0; i < 10; i++ {
key := fmt.Sprintf("key_%d", i)
ts := time.Now().Unix()
im := &item{
value: i,
ts: ts,
}
// 设置缓存
log.Printf("[设置缓存][item][key:%s][v:%v]", key, im)
c.Set(key, im)
}
log.Printf("缓存中的数据量:%d", c.CacheNum())
time.Sleep(33 * time.Second)
log.Printf("缓存中的数据量:%d", c.CacheNum())
// 更新缓存
for i := 0; i < 5; i++ {
key := fmt.Sprintf("key_%d", i)
ts := time.Now().Unix()
im := &item{
value: i,
ts: ts,
}
// 设置缓存
log.Printf("[更新缓存][item][key:%s][v:%v]", key, im)
c.Set(key, im)
}
log.Printf("缓存中的数据量:%d", c.CacheNum())
select {}
}
开源cache
package main
import (
"fmt"
"github.com/patrickmn/go-cache"
"time"
)
func main() {
// Create a cache with a default expiration time of 5 minutes, and which
// purges expired items every 10 minutes
c := cache.New(30*time.Second, 5*time.Second)
// eSet the value of the key "foo" to "bar", with the default xpiration time
// 默认30s,执行5s一次
c.Set("k1", "v1", 31*time.Second)
res, ok := c.Get("k1")
fmt.Println(res, ok)
time.Sleep(time.Second * 32)
res, ok = c.Get("k1")
fmt.Println(res, ok)
}
解决模块导入失败的问题:
go env -w GOPROXY=https://goproxy.cn,direct
➜ ~ go env |grep GOPROXY
GOPROXY="https://goproxy.cn,direct"
➜ ~ go get cloud.google.com/go/compute/metadata
go env -w GO111MODULE=on
传统的 $GOPATH/src 现在已经不用了,然后就是 Go modules 的缓存虽然目前还暂时存放到 $GOPATH/pkg/mod 中,但是在未来就有可能移动到 $GOCACHE/mod 中了,所以建议你应该尽可能地摆脱 GOPATH 才对。如果老哥你感兴趣有时间想深入了解 Go modules 的话,建议老哥去看一下这周四我在【Go 夜读】做的一期比较深入的在线直播分享,包含了 Go modules 的由来之类的,
GoLand 在项目设置中应该叫“Go Modules (vgo)”的选项,把里面的“Proxy”设置为https://goproxy.cn就好了,如果你是 Go 1.13 的话就设置成https://goproxy.cn,direct。
在终端配置:
go env
go env -w GO111MODULE=on
go env -w GOPROXY="https://goproxy.cn,direct"
go env |grep GOPROXY
go get "github.com/orcaman/concurrent-map"
go mod init
go mod tidy
任务高并发
python
celery 队列 + rabbitmq 实现 之前 讲过直播day13
go
channel 类似celery + goruning 实现
channel:
package main
import (
"log"
"time"
)
func main() {
data := make(chan int)
// 读取数据任务
go func() {
// 双变量形式
for {
if r, ok := <-data; ok {
log.Printf("[接收到了数据,并开始处理]: %v", r)
} else {
log.Printf("chan关闭了")
break
}
}
// 单变量
//for r:=range data{
// log.Printf("[接收到了数据,并开始处理]: %v", r)
//}
//log.Printf("chan关闭了")
// 死循环 类似while
//for {
// r := <-data
// log.Printf("[接收到了数据,并开始处理]: %v", r)
//}
}()
// 写入数据
data <- 1
time.Sleep(2 * time.Second)
data <- 2
time.Sleep(2 * time.Second)
data <- 3
time.Sleep(2 * time.Second)
// 现象是 chan 关闭了 没打印
close(data)
// 加入等待1s 发现 chan关闭了 打印了 等待其他进程执行完毕
time.Sleep(1 * time.Second)
}
带缓冲区的chan 主线程不退出方式二:
package main
import (
"log"
"time"
)
func main() {
// 初始化一个类型int的chan,缓冲区为3
data := make(chan int,3)
quit:=make(chan bool) // 达成和最后的time.sleep一样的效果 阻塞主线程,防止异步任务中有未处理完的就退出 类似Python 多线程join
// 读取数据任务
go func() {
for d:=range data{
log.Printf("[接收到了数据,并开始处理]: %v", d)
}
log.Printf("chan关闭了")
// 本任务处理完了,告诉主线程可以退出了
quit <- true
}()
// 写入数据
data <- 1
time.Sleep(2 * time.Second)
data <- 2
time.Sleep(2 * time.Second)
data <- 3
time.Sleep(2 * time.Second)
data <- 4
data <- 5
// 现象是 chan 关闭了 没打印
close(data)
<-quit
// 加入等待1s 发现 chan关闭了 打印了 等待其他进程执行完毕
//time.Sleep(1 * time.Second)
}
channel 同步 异步
说明2个问题:
- 1、quit 是等待所有子进程结束,发送一个结束的消息,主进程退出
- 2、channel缓冲区,设置只能处理3个,同时只有三个在处理,只有处理完了其他任务才会进入处理
package main
import (
"log"
"time"
)
func main() {
// 初始化一个类型int的chan,缓冲区为3
data := make(chan int,3)
quit:=make(chan bool) // 达成和最后的time.sleep一样的效果 阻塞主线程,防止异步任务中有未处理完的就退出 类似Python 多线程join
// 读取数据任务
go func() {
for d:=range data{
time.Sleep(2*time.Second)
log.Printf("[接收到了数据,并开始处理]: %v", d)
}
log.Printf("chan关闭了,但是我还有清理工作,等我5秒钟")
time.Sleep(5 * time.Second)
// 本任务处理完了,告诉主线程可以退出了
quit <- true
}()
// 写入数据
data <- 1
time.Sleep(2 * time.Second)
data <- 2
time.Sleep(2 * time.Second)
data <- 3
time.Sleep(2 * time.Second)
data <- 4
log.Printf("发布4")
data <- 5
log.Printf("发布5")
data <- 6
log.Printf("发布6")
data <- 7
log.Printf("发布7")
data <- 8
log.Printf("发布8")
data <- 9
log.Printf("发布9")
data <- 10
log.Printf("发布10")
// 现象是 chan 关闭了 没打印
close(data)
<-quit
log.Printf("真正推出了")
// 加入等待1s 发现 chan关闭了 打印了 等待其他进程执行完毕
//time.Sleep(1 * time.Second)
}
关闭的channel 返回是空值,bool类型就是false、int 0
01 close nil 的chan 02 向已关闭的chan 再次close 03 向已关闭的chan 再次写入值
package main
func main() {
// panic: close of nil channel
//var c1 chan int
//close(c1)
// panic: close of closed channel
//c2 := make(chan int)
//close(c2)
//close(c2)
// panic: send on closed channel
c3 := make(chan int)
close(c3)
c3 <- 1
}
- close(ch)关闭所有下游协程
- 使用select处理多个channel
- select可以同时监控多个通道的情况,只处理未阻塞的case。当通道为nil时,对应的case永远为阻塞,无论读写
package main
import (
"log"
"os"
"os/signal"
"syscall"
"time"
)
var quitC = make(chan struct{})
func signalWork() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
// 当c中读取到值的时候,说明有人发了信号
sig := <-c
//通知所有读取 quitC的任务
close(quitC)
time.Sleep(2*time.Second)
log.Printf("接收到了停止的信号,信号是:%v,pid=%d,要推出了", sig, os.Getpid())
}
func work01() {
ticker := time.NewTicker(time.Second * 5)
for {
select {
case <-ticker.C:
log.Printf("[我是work01]【5秒周期到了】,干活")
case <-quitC:
log.Printf("[我是work01]【接收到主进程退出信号】,进行清理操作")
return
}
}
}
func work02() {
ticker := time.NewTicker(time.Second * 5)
for {
select {
case <-ticker.C:
log.Printf("[我是work02]【5秒周期到了】,干活")
case <-quitC:
log.Printf("[我是work02]【接收到主进程退出信号】,进行清理操作")
return
}
}
}
func work03() {
// 定时器,定时5秒执行
ticker := time.NewTicker(time.Second * 5)
for {
select {
// 当时间到了5秒钟 就执行,就是5秒执行一次
case <-ticker.C:
log.Printf("[我是work03]【5秒周期到了】,干活")
case <-quitC:
log.Printf("[我是work03]【接收到主进程退出信号】,进行清理操作")
return
}
}
}
func main() {
go work01()
go work02()
go work03()
signalWork()
}
流程控制 if多条件
package main
import "fmt"
func main() {
x := 10
y := "ok"
if x == 10 && y == "ok" {
fmt.Println("两个都满足")
}else if x>10 || y=="ok"{
fmt.Println("任意一个")
}
/*
if x > 10 {
} else if x == 10 {
} else {
}
*/
}
局部变量,变量作用域
package main
import (
"fmt"
"log"
)
func main() {
m := map[string]string{
"region": "bj",
"idc": "世纪互联",
}
if idc := m["idc"]; idc == "世纪互联" {
log.Printf("机房:%v", idc)
}
idc1 := m["idc"]
if idc1 == "世纪互联" {
log.Printf("机房:%v", idc1)
}
//fmt.Println(idc)
fmt.Println(idc1)
}
swith...case 用法
package main
import (
"fmt"
)
func jj(s string){
switch s {
case "go":
fmt.Println("go")
case "python":
fmt.Println("python")
case "java":
fmt.Println("java")
default:
fmt.Println("unkown")
}
}
func main() {
jj("py")
jj("go")
jj("java")
}
fallthrough 不重要:基本用不到,会继续往下面判断打印
package main
import (
"fmt"
)
func jj(s string) {
switch s {
case "go":
fmt.Println("go")
// case接多个值
case "python", "py":
fmt.Println("python")
// 继续判断
fallthrough
case "java":
fmt.Println("java")
default:
fmt.Println("unkown")
}
}
func main() {
jj("py")
jj("go")
jj("java")
}
循环控制 只有for Python中有for while 单条件判断
package main
import (
"log"
)
func main() {
var a,b int
b = 10
for a<b{
log.Printf("我干活呢",a,b)
a++
}
// 死循环
//for {
// log.Printf("我干活呢?")
// time.Sleep(3 * time.Second)
//
//}
}
for 的三种用法:
package main
import "log"
func main() {
for i:=0;i<5;i++{
log.Printf("第一种 三段全的")
}
for i:=0;i<5;{
log.Printf("第二种 自增写在里面")
i++
}
var i int
for ;i<5;{ // or for i<5
log.Printf("第三种 自增写在里面,初始化写在上面")
i++
}
}
切片、map(dict)通过for进行遍历:
package main
import "log"
func main() {
s1:=[]int{10,20,30,40,50}
m1:=map[string]string{"k1":"v1","k2":"v2","k3":"v3"}
for index:=range s1{
log.Printf("[切片]range遍历单变量,只有索引,[index:%v]",index)
}
for index,value:=range s1{
log.Printf("【切片】range遍历双变量,[index:%v][value:%v]",index,value)
}
for key:=range m1{
log.Printf("[map]range遍历单变量,只有key,[key:%v]",key)
}
for key,value:=range m1{
log.Printf("[map]range遍历双变量,[key:%v][value:%v]",key,value)
}
}
golang:
command +D 复制一行
command+shift+?多行注释
command+? 单行注释
切片是指针,而且要赋值,会有问题,解决 (for的内部实现机制会重用value )
package main
import "log"
func main() {
a1 := make([]*int, 3)
a2 := make([]int, 3)
for k, v := range []int{1, 2, 3} {
// 对v重新赋值可以解决
v := v
log.Printf("[v的值:%v][v的地址:%p]", v, &v)
a1[k] = &v
a2[k] = v
}
for i := range a1 {
log.Printf("[指针切片的值为:%v]", *a1[i])
}
for i := range a2 {
log.Printf("[指针切片的值为:%v]", a2[i])
}
}
continue 和 break
package main
import (
"fmt"
"log"
"strings"
)
func main() {
// map无序遍历 有序 按照key去遍历==map中有讲 先通过切片 在通过切片再去遍历
m1 := make(map[string]string)
for i := 0; i < 30; i++ {
key := fmt.Sprintf("%d_key", i)
value := fmt.Sprintf("%d_value", i)
m1[key] = value
}
for k, v := range m1 {
if strings.HasPrefix(k, "1") {
log.Printf("[遇到1就continue]")
continue
}
if k == "23_key" {
log.Printf("[遇到23就break]")
break
}
log.Printf("[正常处理数据【%v=%v】]", k, v)
}
}
goto 下面例子会一直打印 一般不建议使用,少用goto
package main
import "log"
func main() {
i:=0
sum:
log.Printf("[i=%d]",i)
i++
goto sum
}
打印100内值
package main
import "log"
func main() {
i:=0
sum:
{
log.Printf("[i=%d]",i)
i++
}
if i<100{
goto sum
}
}
函数
- go是强类型语言,需要指定返回值类型
package main
import (
"fmt"
)
func max(n1, n2, int) int {
if n1 > n2 {
return n1
}
return n2
}
func main() {
fmt.Println(max(1,10))
fmt.Println(max(-1,2))
}
函数参数只是var声明,map要使用还需要make一下
package main
import "fmt"
// 返回参数命令 只是var 申明,使用前需要make一下
func f1()(names []string,m map[string]int,num int){
m = make(map[string]int)
m["k1"] = 2
return
}
func main() {
a,b,c :=f1()
fmt.Println(a,b,c)
}
变长参数:
package main
import "log"
// 变长参数 返回最小值
func min(a ...int)int{
if len(a) == 0{
return 0
}
min:=a[0]
for _,v :=range a{
if v<min{
min =v
}
}
return min
}
func main() {
x1:=min(1,7,8,9,23,9)
log.Printf("[直接传多个参数]:%d",x1)
s1:=[]int{2,3,4,6,23,6,23}
x2:=min(s1...)
log.Printf("[数组传参]:%d",x2)
}
参数默认是值类型,copy传递
如果要引用传递,就传递指针
如果是引用类型 就是引用传递
package main
import (
"log"
"time"
)
// 值类型 值传递
func add1(num int) {
log.Printf("[值传递][传入的参数值为:%d]", num)
num++
log.Printf("[值传递][add1计算后的值为:%d]", num)
}
// 值类型 引用传递
func add2(num *int) {
log.Printf("[引用传递][传入的参数值为:%d]", *num)
*num++
log.Printf("[引用传递][add2计算后的值为:%d]", *num)
}
func main() {
num := 1
log.Printf("[局部遍历的值:%d]", num)
add1(num)
time.Sleep(1 * time.Second)
log.Printf("[局部遍历的值:%d]", num)
add2(&num)
time.Sleep(1 * time.Second)
log.Printf("[局部遍历的值:%d]", num)
}
map/切片都是引用传递:
package main
import (
"log"
)
// 引用类型 引用传递
func mod(s1 []int, m1 map[string]string) {
log.Printf("[引用传递][传入参数为:%v %v]", s1, m1)
s1[0] = 100
m1["a"] = "a2"
log.Printf("[引用传递][函数内部处理完为:%v %v]", s1, m1)
}
func main() {
s1 := []int{1, 2, 3}
m1 := map[string]string{"a": "a1", "b": "b1"}
mod(s1, m1)
log.Printf("[引用传递][函数外部处理完为:%v %v]", s1, m1)
}
匿名函数 不带参数
package main
import "fmt"
func main() {
f := func() {
fmt.Println("abdc")
}
f()
fmt.Printf("%T", f)
}
// 带参数 + 返回值的
func main() {
f := func(args string) string {
fmt.Println("abdc")
return "abcd"
}
a:=f("abcd")
fmt.Printf("%T,%v", f,a)
}
闭包准备:
package main
import (
"fmt"
)
func FGen(x, y int) (func() int, func(int) int) {
// 求和匿名函数
sum := func() int {
return x + y
}
// 求平均值的匿名函数
arg:=func(z int)int{
return (x+y)*z
}
return sum,arg
}
func main() {
f1,f2:=FGen(1,2)
fmt.Println(f1())
fmt.Println(f2(3))
}
闭包函数 (函数嵌套) 不同闭包维护不同变量,跟其他语言闭包一样,都是内层函数调用外层变量,保持外层func不释放,内层执行完毕后才会释放外层函数 保持外层变量不释放
package main
import "fmt"
func Greeting()func(string)string{
a:="你好啊"
return func(s string) string {
a+=s
return a
}
}
func main() {
g1 := Greeting()
g2 := Greeting()
fmt.Println(g1("小姨"))
fmt.Println(g1("李鬼"))
fmt.Println(g1("松江"))
fmt.Println(g2("拉ex"))
}
闭包做装饰器 增加扩展性,所有语言都是通用的,解耦,函数式编程比较重要的点:
package main
import (
"log"
)
func add1() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
log.Printf("[普通的累加器]:%d", sum)
}
func add2() func(int) int {
// 自由变量
sum := 0
return func(i int) int {
sum += i
return sum
}
}
func callCF() {
f := add2()
sum := 0
for i := 0; i < 10; i++ {
sum = f(i)
log.Printf("[闭包的累加器]:%d", sum)
}
}
func main() {
add1()
// 闭包的
callCF()
}
递归必须使用闭包,之前练习递归貌似没有使用闭包 是闭包,返回是一个函数,看来这里理解还不够
递归:斐波拉切数列
func fib(i int)int{
if i==0||i==1{
return i
}
return fib(i-1)+fib(i-2)
}
func main() {
log.Println(fib(7))
// 递归退出条件
for i:=0;i<10;i++{
log.Printf("[%d=%d]",i,fib(i))
}
}
闭包 + 递归
package main
import "log"
// 实现1+..100
// 从低位加到高位
func sum1(num int)int{
if num == 100{
return num
}
return num + sum1(num+1)
}
// 从高位加到低位
func sum2(num int)int{
if num == 1{
return num
}
return num + sum2(num-1)
}
func main() {
log.Printf("[从低位加到高位]:%d",sum1(1))
log.Printf("[从高位加到低位]:%d",sum2(100))
}
defer 类似Python 上下文管理 with openxx as f enter exit
保证文件描述符被close
package main
import "fmt"
func main() {
for i:=0;i<5;i++{
// 后进先出 栈操作
defer fmt.Println(i)
}
}
defer打印时间差
package main
import (
"log"
"time"
)
func main() {
start := time.Now()
log.Printf("开始时间为:%v", start)
defer log.Printf("时间差:%v", time.Since(start))
time.Sleep(3 * time.Second)
log.Printf("函数结束")
}
解决:通过引用传递匿名函数解决:defer func
defer与return关系
值传递 深拷贝
return xxx 这条语句不是一条原子操作
- 先给返回值赋值
-return 1 要翻译成 res=1 +return
- 调用defer语句
- 返回调用函数中
重点:
map 带过期时间、分片、sync.map
chan goroutine一起理解
函数
下一节内容:
panic和defer
结构体 == 类
面向对象
接口
https://github.com/orcaman/concurrent-map
MarkDown实现
生成目录的方法:
* [1.语法示例](#1)
* [1.1图片](#1.1)
* [1.2换行](#1.2)
* [1.3强调](#1.3)
使用markdown语法:[点击跳转](#jump)