Skip to content

go

21 posts with the tag “go”

Go中的数据竞争和竞争条件

并发是一个有趣的词,同时我们还可能听过其他词,如:“并行”,“串行”,“线程”,“多线程”等。在计算机科学中,并发是一个宽泛的话题,涉及到许多的主题。很有必要弄清楚他们的定义和区别。

并发 (Concurrent) : 多个事情在同时发生的过程,这个“同时”具体可以是一个时间段,也就是在同一时间段内多个事情正在进行。

并行 (Parallel) : 在同一时刻,多个事情同时在进行。

具体到操作系统中,当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。

Concurrent

当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

Parallel

引用Ealang之父Joe Aramstrong 给5岁小孩解释并发和并行间区别

difference

并发是两个队列交替使用一台咖啡机,并行是两个队列同时使用两台咖啡机。

并发的多个任务间是相互抢占资源的,而并行的多个任务是不相互抢占资源的。

  • 指串行通信(Serial communication)是指在计算机总线或其他数据通道上,每次传输一个位元数据,并连续进行以上单次过程的通信方式。与之对应的是并行通信,它在串行端口上通过一次同时传输若干位元数据的方式进行通信
  • 和并行相对应,做某个事情时,一个步骤一个步骤的进行处理

1.4、数据竞争(data race)和竞争条件(race condition)

Section titled “1.4、数据竞争(data race)和竞争条件(race condition)”

当计算机程序中有多个同时执行的代码路径时,就会出现竞争条件(race condition),如果多个代码执行路径花费的时间和预期不同,它们可能以与预期不同的顺序完成,这可能由于意外的行为导致软件bug。

数据竞争是一种竞争条件。数据竞争是各种形式内存模型的重要组成部分。C11C++11 标准中定义的内存模型指定包含数据竞争的 C 或 C++ 程序具有未定义的行为(undefined behavior )。

Go在1.19版本,修改了内存模型的定义,使它的内存模型同其他语言,如:C、C++、Java、JavaScript、Rust和Swift等保持一致。

竞争条件可能难以重现和调试,因为最终结果是不确定的,并且取决于干扰线程之间的相对时间。在调试模式下运行、添加额外的日志记录或者附加调试器时, 这种性质的问题可能因此而消失,因此最好通过仔细的软件设计来避免竞争条件。

数据竞争(data race) 的精准定义特定于所使用的并发模型,但通常情况指的是一种情况,即一个线程中的内存操作可能尝试访问内存位置,同时另一个线程中的内存操作正在写入改内存位置,这种情况下这是危险的。 这意味着数据竞争和竞争条件不同,因为即使在没有数据竞争的程序中,由于时序,也可能具有不确定性,如,在所有内存访问都使用原子操作的程序中。

许多平台中下面的操作是危险的,如果两个线程同时写入一个内存位置,这个内存地址可能保存一个值,该值是每个线程尝试写入值的bit位的任意且无意义的组合; 如果生成的值都不是两个线程试图写入的值,则可能导致内存损坏。同样的,一个线程从某个位置读取,而另一个线程正在写入该位置,则有可能返回一个值,这个值可能是写入值的bits位和已有值的bits位的任意且无意义的组合值。

Go语言中的每一个并发执行单元叫做协程(goroutine),一个程序启动时,其主函数即在一个单独的goroutine中运行,也叫它main goroutine。goroutine是轻量级的,一个goroutine会以一个很小的栈开始生命周期,一般只需要2KB,和操作OS线程一样,会保存其活跃或挂起的函数调用的本地变量。 不一样的地方是,一个goroutine的栈大小不固定,根据需要动态伸缩。

Go支持两种并发模型,一种是CSP(Communicating Sequential Process,通信顺序进程)模型,另一种是内存同步访问。

Go的内存模型中也有上述的数据竞争和竞争条件。

当两个或多个操作必须按照正确的顺序执行,而程序并未保证这个顺序,就会发生竞争条件。

当有两个或者多个 goroutine 同时访问一个变量,其中至少有一个访问是写入时,就会发生数据竞争(data race)。

如下面是一个可能导致崩溃和内存损坏的数据竞争示例:

func main() {
c := make(chan bool)
m := make(map[string]string)
go func() {
m["1"] = "a" // First conflicting access.
c <- true
}()
m["2"] = "b" // Second conflicting access.
<-c
for k, v := range m {
fmt.Println(k, v)
}
}

为了帮助诊断这类bug,Go内置了一个数据竞争检测器,可以通过添加-race 标志到go command中来使用它:

Terminal window
$ go test -race mypkg
$ go run -race mysrc.go
$ go build -race mycmd
$ go install -race mypkg

当竞争检测器在程序中发现数据竞争时,它会打印一份报告,其中包含冲突访问的堆栈跟踪,以及创建关联goroutine的堆栈,如:

Terminal window
$ go run --race datarace/main.go
==================
WARNING: DATA RACE
Write at 0x00c000112180 by goroutine 7:
runtime.mapassign_faststr()
src/runtime/map_faststr.go:203 +0x0
main.main.func1()
atmoic-pointer/datarace/main.go:9 +0x50
Previous write at 0x00c000112180 by main goroutine:
runtime.mapassign_faststr()
src/runtime/map_faststr.go:203 +0x0
main.main()
atmoic-pointer/datarace/main.go:12 +0x132
Goroutine 7 (running) created at:
main.main()
atmoic-pointer/datarace/main.go:8 +0x115
==================
1 a
2 b
Found 1 data race(s)
exit status 66
  • 如果多个goroutine并发访问修改数据时,必须保证各操作序列化执行(串行执行)
  • 为了序列化执行,可以使用channel或者其他同步原语(如sync 和 sync/atomic包里提供的)来保护被共享的数据

sync等包提供了Mutex(互斥锁)、RWMutext(读写锁)、Once、WaitGroup、Atomic Values 等传统锁机制,但Go语言更推荐使用channel进行同步通信,“不要通过共享内存来通信,相反,通过通信来共享内存”

Do not communicate by sharing memory; instead, share memory by communicating. - effective go

追求简洁,尽量使用channel,并且认为goroutine是低成本的。

Golang性能分析-pprof

遵从代码最佳实践或者代码规范指导,可以避免一些参见的坑,但在运行代码时遇到各种问题后,还是需要深入代码了解代码性能和瓶颈, 以便能够发现问题、解决问题和做出相应的优化。

性能分析器(Profiler)是一种动态的性能分析工具,可以提供程序在运行过程中的各种维度的关键性能分析,可以用来解决各种性能问题,定位内存泄露, 线程竞争死锁等问题。

Go运行时以pprof可视化工具预期的格式提供性能分析数据,分析数据可以在测试期间通过go test,或者从net/http/pprof包提供的endpoint收集。我们需要收集性能分析数据,并通过pprof工具对程序进行分析,并支持可视化数据。

Go 通过runtime/pprof包提对profile提供了内置的支持:

  • CPU: CPU分析采集一定周期内程序对CPU的使用情况,确定程序在主动消耗CPU周期是如何消耗时间的
  • Heap: Heap分析,记录内存分配,用于监视当前和历史内存使用情况,以及检查内存泄露
  • Goroutine: Goroutine分析报告所有当前正在运行goroutine的堆栈跟踪信息
  • threadcreate: 报告程序中导致创建新OS线程的堆栈跟踪信息
  • Block: 阻塞分析,显示goroutine阻塞等待同步(包括定时器通道)的位置,默认没有启用,可以使用 runtime.SetBlockProfileRate 启用它
  • Mutex: 互斥锁分析,报告互斥锁的竞争情况,当认为互斥锁争用导致CPU没有得到充分使用时,可以使用这种分析,默认没有启动,可以使用runtime.SetMutexProfileFraction 启用它

目前有三种方式进行数据采集:

  • go test: 通过运行测试用例继续采集
  • runtime/pprof: 直接在代码中指定采集程序运行数据
  • net/http/pprof: 采集http server运行时数据进行分析

测试标准库在go test中内置了对采集性能分析数据的内置支持,如下面的命令,在当前目录中运行测试,并将CPU和内存采样数据分别写入cpu.prof和 mem.prof:

Terminal window
go test -cpuprofile cpu.prof -memprofile mem.prof .

在长时间运行的http服务中,只需要导入net/http/pprof包,服务便可以进行采集数据,并多出一个终结点(endpoint)/debug/pprof,可用于观察采集到的 性能分析数据。包导入语句如:

_ "net/http/pprof"

net/http/pprof 包初始化时在DefaultServeMux中为给定的URL注册了处理程序

func init() {
http.HandleFunc("/debug/pprof/", Index)
http.HandleFunc("/debug/pprof/cmdline", Cmdline)
http.HandleFunc("/debug/pprof/profile", Profile)
http.HandleFunc("/debug/pprof/symbol", Symbol)
http.HandleFunc("/debug/pprof/trace", Trace)
}

这些handler将采样数据写入到http.ResponseWriter,导入net/http/pprof后需要做的,只是启动一个HTTP server就可以了,如:

http.ListenAndServe(":8080", nil)

完整的最小采集demo:

package main
import (
"log"
"net/http"
"net/http/httputil"
_ "net/http/pprof"
)
func main() {
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
c, _ := httputil.DumpRequest(r, true)
log.Println(string(c))
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(201)
rw.Write([]byte("{}"))
})
http.ListenAndServe(":8080", nil)
}

运行程序后,就可以在浏览器访问localhost:8080/debug/pprof实时采样数据总览页面:

pprof

  • allocs: 所有已分配内存的采样
  • block: 导致阻塞同步的堆栈跟踪
  • cmdline: 显示程序启动的路径命令和参数
  • goroutine: 所有当前goroutine的堆栈跟踪
  • heap: 当前活动对象的内存采样
  • mutext: 互斥锁持有者的堆栈跟踪
  • profile: 进行CPU采样,采样完成得到一个供分析用的profile文件,默认采集间隔是30秒
  • threadcreate: 创建新OS线程的堆栈跟踪
  • trace: 程序的执行跟踪信息

直接通过上面的方式查看采样数据枯燥且缺乏直观性,后面介绍可以借助go tool pprof命令,可以更直观方便的分析采样数据。

通过使用runtime/pprof包,可以直接在代码中进行采样,如可以使用pprof.StartCPUProfile(io.Writer)开始CPU采样, 使用pprof.StopCPUProfile() 停止CPU采样,如:

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
func main() {
flag.Parse()
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
defer f.Close() // error handling omitted for example
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("could not start CPU profile: ", err)
}
defer pprof.StopCPUProfile()
}
// ... rest of the program ...
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
log.Fatal("could not create memory profile: ", err)
}
defer f.Close() // error handling omitted for example
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal("could not write memory profile: ", err)
}
}
}

运行程序:

Terminal window
go run -cpuprofile cpu.prof -memprofile mem.prof .

将CPU和内存采样数据分别写入文件cpu.prof 和 mem.prof。

通过go tool pprof命令在交互命令行终端中使用采用数据,对于采样数据文件profile可以使用:

Terminal window
go tool pprof profile

profile

对于通过「采样方式2.2」HTTP 终结点方式,可以使用go tool pprof url的方式,如:

Terminal window
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

profile-2

采样url,在可以在采样总览页面中获取到:

ulr

使用pprof可视化界面时,在go tool pprof命令通过指定端口参数-http,运行一个pprof分析用的站点,如:

  • 查看当前所有goroutine的堆栈跟踪
go tool pprof -http :9090 'http://localhost:8080/debug/pprof/goroutine'

pprof ui

通过pprof提供的可视化界面,可以更清晰直观的查看调用链等信息。

  • Top top

  • Graph graph

  • Flame Graph flame

  • Peek peek

  • Source source

  • Disassemble disassemble

NeoVim调试Python、Golang

在上两篇中NeoVim开发环境配置NeoVim配置Go开发环境(进阶), 完成了将NeoVim作为IDE最基本常用功能的,这里将进一步完成对Python、Golang调试环境的配置。

在NeoVim上进行调试(Python、Golang)有nvim-dapvimspector可做选择,这里 将使用vimspector进行调试工作。

在plug.vim中添加:

Plug 'puremourning/vimspector'

:PlugInstall安装插件。 在init.vim中,添加:

let g:vimspector_enable_mappings='HUMAN'

将vimspector 快捷键映射配置为HUMAN模式:

KeyMappingFunction
F5<Plug>VimspectorContinueWhen debugging, continue. Otherwise start debugging.
F3<Plug>VimspectorStopStop debugging.
F4<Plug>VimspectorRestartRestart debugging with the same configuration.
F6<Plug>VimspectorPausePause debuggee.
F9<Plug>VimspectorToggleBreakpointToggle line breakpoint on the current line.
<leader>F9<Plug>VimspectorToggleConditionalBreakpointToggle conditional line breakpoint or logpoint on the current line.
F8<Plug>VimspectorAddFunctionBreakpointAdd a function breakpoint for the expression under cursor
<leader>F8<Plug>VimspectorRunToCursorRun to Cursor
F10<Plug>VimspectorStepOverStep Over
F11<Plug>VimspectorStepIntoStep Into
F12<Plug>VimspectorStepOutStep out of current function scope

下面是各个语言调试使用的Adapter:

通过:VimspectorInstall debugpy:VimspectorInstall delve 完成对python、go 调试adapter的安装。

由于NeoVim的LSP指定当前根目录是各LSP Server提供的功能,在调试Ptyon时,在目录中添加.pyrightconfig.json文件:

{
"executionEnvironments": [
{"root": "."}
]
}

如上配置,将.pyrightconfig.json文件所在目录,作为Root目录。

配置调试器,添加配置文件 .vimspector.json:

{
"configurations": {
"run": {
"adapter": "debugpy",
"default": true,
"configuration": {
"request": "launch",
"type": "python",
"cwd": "${workspaceRoot}",
"stopOnEntry": true,
"program": "${file}"
},
"breakpoints": {
"exception": {
"raised": "N",
"uncaught": "",
"userUnhandled": ""
}
}
}
}
}

configuration.program指定要调试的程序,可以通过configuration.env配置调试时程序的环境变量,configuration.args配置参数。 如在调试一个scrapy的爬虫程序时,通过program指定scrapy位置,args指定启动的具体spider:

{
"configurations": {
"run": {
"adapter": "debugpy",
"default": true,
"configuration": {
"request": "launch",
"type": "python",
"program": "~/opt/miniconda3/envs/p3/bin/scrapy",
"cwd": "${workspaceRoot}",
"stopOnEntry": true,
"args": ["crawl", "${fileBasenameNoExtension}"]
},
"breakpoints": {
"exception": {
"raised": "N",
"uncaught": "",
"userUnhandled": ""
}
}
}
}
}

配置调试器,添加配置文件 .vimspector.json,如:

{
"configurations": {
"default": {
"adapter": "delve",
"default": true,
"configuration": {
"request": "launch",
"mode": "debug",
"cwd": "${workspaceRoot}",
"stopOnEntry": true,
"program": "${file}",
"args": [
"-http_addr","localhost:8084"
]
},
"breakpoints": {
"exception": {
"raised": "N",
"uncaught": "",
"userUnhandled": ""
}
}
}
}
}

指定adapter为delve来调试go程序,program指定要调试的程序文件,args指定参数http_addr=localhost:8084

  1. vimspector
  2. Debugging python in neovim
  3. Neovim — Debugging Application
  4. Programming Go in Neovim
  5. nvim-lsconfig Configurations
  6. vim-delve
  7. golang: debugging application in neovim
  8. nvim-dap
  9. debug-adapter-configuration

NeoVim配置Go开发环境

在上一篇NeoVim开发环境配置中记录了NeoVim的基本插件配置,随着了解的深入,这里对NeoVim的插件配置进行一步优化处理。

首先对配置进行切分,避免配置都混杂在init.vim中,不利于管理,下面是我的配置文件结构:

Terminal window
$HOME/.config/nvim/
├── after
└── plugin
├── coq_nvim.rc.vim
├── defx.rc.vim
├── fugitive.rc.vim
├── lspconfig.rc.vim
└── treesitter.rc.vim
├── init.vim
├── macos.vim
├── maps.vim
└── plug.vim

init.vim: 根配置文件

macos.vim: mscOS特殊配置

plug.vim :Vim-plug插件配置

maps.vim:快捷键配置

after/plugin: 放置插件的配置脚本,这里面的文件也会在vim每次启动的时候加载,不过是等待plugin加载完成之后才加载after里的内容,所以叫做after。VIM USER MANUAL runtimepath

安装 vim-plug$HOME/.local/share/nvim/site/autoload/plug.vim:

Terminal window
sh -c 'curl -fLo $HOME/.local/share/nvim/site/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'

我在$HOME/.config/nvim/plug.vim管理配置插件:

if has("nvim")
let g:plug_home = stdpath('data') . '/plugged'
endif
call plug#begin()
...
call plug#end()

为了使plug.vim配置生效,需要在init.vim中进行配置加载:

runtime ./plug.vim

此时,重启NeoVim,运行:PlugInstall进行插件安装。

此外其他几个配置文件:maps.vim,macos.vim 也需要手动配置加载:

if has("unix")
let s:uname = system("uname -s")
" Do mac stuff
if s:uname == "Darwin\n"
runtime ./macos.vim
endif
endif
runtime ./maps.vim

后续安装的插件,分个单独配置,放置到after/plugin目录下。

至此root配置(init.vim)、插件管理、快捷键管理、各插件独立进行配置,配置分离便于管理。

NERDTree是Vim最常用的插件之一,可以在Vim运行时显示目录和文件结构,类似TextMate左侧的文件浏览器,但操作起来更为方便。

目前还有另外一个目录和文件结构管理插件defx.nvim

  • 不依赖于 denite.nvim
  • Vim8/neovim 兼容(Vim8 需要 nvim-yarp)
  • 由 Python3 实现
  • 没有双重过滤器功能
  • 列功能
  • 类似于 denite.nvim 一样支持 source
  • 支持选项
  • 突出显示由列定义
  • 很少的命令(仅:Defx 命令?)
  • 扩展重命名
  • 支持标记
Plug 'Shougo/defx.nvim', { 'do': ':UpdateRemotePlugins' }

安装完成后,使用:Defx命令来使用:

img

每次使用时Buffer会充满整个窗口,并不是我们常用形态,此时需要再进行配置,指定显示的位置以及大小等:

call defx#custom#option('_', {
\ 'winwidth': 38,
\ 'direction': 'topleft',
\ 'split': 'vertical',
\ 'show_ignored_files': 0,
\ 'buffer_name': '',
\ 'toggle': 1,
\ 'resume': 1,
\ })

效果如下:

Defx -split=vertical

使用时通过输入Defx命令太过繁琐效率不高,可以通过设置快捷键来改善,maps.vim中添加:

nnoremap <silent>sf :<C-u>Defx
\ -auto-cd
\ -columns=mark:indent:icon:icons:filename:type:git<CR>

后续通过键入sf即可唤出文件目录管理。

after/plugin/defx.rc.vim 添加快捷键配置:

autocmd FileType defx call s:defx_my_settings()
function! s:defx_my_settings() abort
" Define mappings
nnoremap <silent><buffer><expr> <CR>
\ defx#do_action('open')
nnoremap <silent><buffer><expr> c
\ defx#do_action('copy')
nnoremap <silent><buffer><expr> m
\ defx#do_action('move')
nnoremap <silent><buffer><expr> p
\ defx#do_action('paste')
nnoremap <silent><buffer><expr> l
\ defx#do_action('open')
nnoremap <silent><buffer><expr> E
\ defx#do_action('open', 'vsplit')
nnoremap <silent><buffer><expr> P
\ defx#do_action('preview')
nnoremap <silent><buffer><expr> o
\ defx#do_action('open_tree', 'toggle')
nnoremap <silent><buffer><expr> K
\ defx#do_action('new_directory')
nnoremap <silent><buffer><expr> N
\ defx#do_action('new_file')
nnoremap <silent><buffer><expr> M
\ defx#do_action('new_multiple_files')
nnoremap <silent><buffer><expr> C
\ defx#do_action('toggle_columns',
\ 'mark:indent:icon:filename:type:size:time')
nnoremap <silent><buffer><expr> S
\ defx#do_action('toggle_sort', 'time')
nnoremap <silent><buffer><expr> d
\ defx#do_action('remove')
nnoremap <silent><buffer><expr> r
\ defx#do_action('rename')
nnoremap <silent><buffer><expr> !
\ defx#do_action('execute_command')
nnoremap <silent><buffer><expr> x
\ defx#do_action('execute_system')
nnoremap <silent><buffer><expr> yy
\ defx#do_action('yank_path')
nnoremap <silent><buffer><expr> .
\ defx#do_action('toggle_ignored_files')
nnoremap <silent><buffer><expr> ;
\ defx#do_action('repeat')
nnoremap <silent><buffer><expr> h
\ defx#do_action('cd', ['..'])
nnoremap <silent><buffer><expr> ~
\ defx#do_action('cd')
nnoremap <silent><buffer><expr> q
\ defx#do_action('quit')
nnoremap <silent><buffer><expr> <Space>
\ defx#do_action('toggle_select') . 'j'
nnoremap <silent><buffer><expr> *
\ defx#do_action('toggle_select_all')
nnoremap <silent><buffer><expr> j
\ line('.') == line('$') ? 'gg' : 'j'
nnoremap <silent><buffer><expr> k
\ line('.') == 1 ? 'G' : 'k'
nnoremap <silent><buffer><expr> <C-l>
\ defx#do_action('redraw')
nnoremap <silent><buffer><expr> <C-g>
\ defx#do_action('print')
nnoremap <silent><buffer><expr> cd
\ defx#do_action('change_vim_cwd')
endfunction

设置文件图标可以让文件目录使用起来更加舒适,此时需要使用任意一种Nerd Fonts字体,并配置好iTerm,以便在iTerm2中可以正常显示字体图标:

Terminal window
brew tap homebrew/cask-fonts
brew cask install font-hack-nerd-font

iTerm2配置:

image-20220121123108917

这里有一点需要注意的是,如果勾选了Use a different font for non-ASSII text,那么第二种字体也需要使用Nerd Fonts中的一种,不然还是无法正常显示图标。

配置好iTerm2后,还需要安装defx-icons插件,才能使defx.nvim显示出文件图标,plug.vim添加:

Plug 'kristijanhusak/defx-icons'

:PlugInstall安装重启后,就可以看到:

image-20220121123639182

此时发现文件图片和文件名重叠,添加下面配置即可:

let g:defx_icons_column_length = 2

此时效果如下:

image-20220121123849035

使用官方文档中的默认快捷键配置后,发现总是从当前窗口中,打开文件,很不符合习惯:

image-20220121124512001

如果想要和vscode一样,从右侧窗口打开文件,可以使用drop替代open来配置快捷键:

nnoremap <silent><buffer><expr> <CR> defx#do_action('drop')
nnoremap <silent><buffer><expr> l defx#do_action('drop')

文件名太长导致显示问题时,可以通过下面的方式,限制文件名可以显示的最大长度:

call defx#custom#column('filename', {
\ 'max_width': 26,
\})

mhinz/vim-startify 启动屏可以记录最近编辑的文件,使用对应数字编号就可以快速打开文件,使用起来非常方便。

image-20220121125121988

Neovim已经内置了语言服务器协议 (LSP)。LSP时一种开放的、基于JSON- RPC的协议,用于源代码编辑器和语言服务器之间的通信,可以提供特定于编程语言的功能,如:

  1. 调转到定义
  2. 自动完成
  3. 代码操作(自动格式化、包导入…)
  4. 显示方法签名
  5. 显示、转到参考
  6. 代码片段

从 0.5 版本开始,NeoVim 原生支持该协议。NeoVim在nvim-lspconfig 插件中维护了一个配置列表。该存储库包含设置和排除许多服务器故障的说明。

如果需要为自己刚兴趣的编程语言,需要为其安装和配置相应的LSP服务器,这些可以通过nvim-lspconfig插件来处理。

Plug 'neovim/nvim-lspconfig'

需要注意的是这个插件只是配置管理,我们还需要单独为相应的编程语言安装LSP服务器,具体详见server_configurations

安装Google的 golang lsp server见:

https://github.com/golang/tools/tree/master/gopls

激活LSP服务,以及完整配置如下, lspconfig.rc.vim:

require'lspconfig'.gopls.setup{}
if !exists('g:lspconfig')
finish
endif
lua << EOF
vim.lsp.set_log_level("debug")
EOF
lua << EOF
local nvim_lsp = require('lspconfig')
nvim_lsp.pyright.setup{}
nvim_lsp.gopls.setup{}
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities.textDocument.completion.completionItem.snippetSupport = true
-- Use an on_attach function to only map the following keys
-- after the language server attaches to the current buffer
local on_attach = function(client, bufnr)
local function buf_set_keymap(...) vim.api.nvim_buf_set_keymap(bufnr, ...) end
local function buf_set_option(...) vim.api.nvim_buf_set_option(bufnr, ...) end
-- Enable completion triggered by <c-x><c-o>
buf_set_option('omnifunc', 'v:lua.vim.lsp.omnifunc')
-- Mappings.
local opts = { noremap=true, silent=true }
-- See `:help vim.lsp.*` for documentation on any of the below functions
buf_set_keymap('n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<CR>', opts)
buf_set_keymap('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<CR>', opts)
buf_set_keymap('n', 'ga', '<Cmd>lua vim.lsp.buf.code_action()<CR>', opts)
buf_set_keymap('n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)
buf_set_keymap('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', opts)
buf_set_keymap('n', 'K', '<cmd>lua vim.lsp.buf.hover()<CR>', opts)
buf_set_keymap('n', '<C-k>', '<cmd>lua vim.lsp.buf.signature_help()<CR>', opts)
buf_set_keymap('n', '<space>wa', '<cmd>lua vim.lsp.buf.add_workspace_folder()<CR>', opts)
buf_set_keymap('n', '<space>wr', '<cmd>lua vim.lsp.buf.remove_workspace_folder()<CR>', opts)
buf_set_keymap('n', '<space>wl', '<cmd>lua print(vim.inspect(vim.lsp.buf.list_workspace_folders()))<CR>', opts)
buf_set_keymap('n', '<space>D', '<cmd>lua vim.lsp.buf.type_definition()<CR>', opts)
buf_set_keymap('n', '<space>rn', '<cmd>lua vim.lsp.buf.rename()<CR>', opts)
buf_set_keymap('n', '<space>e', '<cmd>lua vim.diagnostic.open_float()<CR>', opts)
buf_set_keymap('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<CR>', opts)
buf_set_keymap('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<CR>', opts)
buf_set_keymap('n', '<space>q', '<cmd>lua vim.diagnostic.setloclist()<CR>', opts)
buf_set_keymap('n', '<space>f', '<cmd>lua vim.lsp.buf.formatting()<CR>', opts)
end
-- Use a loop to conveniently call 'setup' on multiple servers and
-- map buffer local keybindings when the language server attaches
local servers = { 'pyright', 'rust_analyzer', 'tsserver' }
for _, lsp in ipairs(servers) do
nvim_lsp[lsp].setup {
on_attach = on_attach,
flags = {
debounce_text_changes = 150,
}
}
end
nvim_lsp.gopls.setup{
cmd = {'gopls'},
-- for postfix snippets and analyzers
capabilities = capabilities,
settings = {
gopls = {
experimentalPostfixCompletions = true,
analyses = {
unusedparams = true,
shadow = true,
},
staticcheck = true,
},
},
on_attach = on_attach,
}
function goimports(timeoutms)
local context = { source = { organizeImports = true } }
vim.validate { context = { context, "t", true } }
local params = vim.lsp.util.make_range_params()
params.context = context
-- See the implementation of the textDocument/codeAction callback
-- (lua/vim/lsp/handler.lua) for how to do this properly.
local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, timeout_ms)
if not result or next(result) == nil then return end
local actions = result[1].result
if not actions then return end
local action = actions[1]
-- textDocument/codeAction can return either Command[] or CodeAction[]. If it
-- is a CodeAction, it can have either an edit, a command or both. Edits
-- should be executed first.
if action.edit or type(action.command) == "table" then
if action.edit then
vim.lsp.util.apply_workspace_edit(action.edit)
end
if type(action.command) == "table" then
vim.lsp.buf.execute_command(action.command)
end
else
vim.lsp.buf.execute_command(action)
end
end
EOF

配置gopls服务,提供跳转、查看定义、重命名等:

-- Mappings.
local opts = { noremap=true, silent=true }
-- See `:help vim.lsp.*` for documentation on any of the below functions
buf_set_keymap('n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<CR>', opts)
buf_set_keymap('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<CR>', opts)
buf_set_keymap('n', 'ga', '<Cmd>lua vim.lsp.buf.code_action()<CR>', opts)
buf_set_keymap('n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)
buf_set_keymap('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', opts)

format img

格式化代码快捷键配置:

buf_set_keymap('n', '<space>f', '<cmd>lua vim.lsp.buf.formatting()<CR>', opts)

保存文件时自动格式化:

" 保存代码前进行自动格式化
autocmd BufWritePre *.go lua vim.lsp.buf.formatting()

触发包导入有两种方式来解决,一种时通过code action来手动触发:

buf_set_keymap('n', 'ga', '<Cmd>lua vim.lsp.buf.code_action()<CR>', opts)

也可以通过保存文件时,自动触发:

function goimports(timeoutms)
local context = { source = { organizeImports = true } }
vim.validate { context = { context, "t", true } }
local params = vim.lsp.util.make_range_params()
params.context = context
-- See the implementation of the textDocument/codeAction callback
-- (lua/vim/lsp/handler.lua) for how to do this properly.
local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, timeout_ms)
if not result or next(result) == nil then return end
local actions = result[1].result
if not actions then return end
local action = actions[1]
-- textDocument/codeAction can return either Command[] or CodeAction[]. If it
-- is a CodeAction, it can have either an edit, a command or both. Edits
-- should be executed first.
if action.edit or type(action.command) == "table" then
if action.edit then
vim.lsp.util.apply_workspace_edit(action.edit)
end
if type(action.command) == "table" then
vim.lsp.buf.execute_command(action.command)
end
else
vim.lsp.buf.execute_command(action)
end
end

并配置:

autocmd BufWritePre *.go lua goimports(1000)

完成是开箱即用的。我们只需要将它映射到 vimomnifunc即可使我们的Ctrl+x,Ctrl+o工作:

buf_set_option('omnifunc', 'v:lua.vim.lsp.omnifunc')

完成

但是,每次触发都需要通过Ctrl+x,Ctrl+o来触发,十分不方便,为了自动触发,可以在通过安装新的插件ms-jpq/coq_nvim来完成,如:

" 增强代码自动完成
Plug 'ms-jpq/coq_nvim', {'branch': 'coq'}
" 9000+ Snippets
Plug 'ms-jpq/coq.artifacts', {'branch': 'artifacts'}
Plug 'ms-jpq/coq.thirdparty', {'branch': '3p'}

始终将所有操作复制到系统剪贴板:

set clipboard+=unnamedplus

目前基于内置lsp的功能已经比较完善,但是缺乏一个良好的操作UI,如重命名时,输入新名字的地方在窗口底部:

image-20220121173142281

使用async-lsp-finder后:

img

安装

Plug 'glepnir/lspsaga.nvim'

快捷键配置:

" 异步lsp查找
nnoremap <silent> gh :Lspsaga lsp_finder<CR>
" Code Action
nnoremap <silent><leader>ca :Lspsaga code_action<CR>
vnoremap <silent><leader>ca :<C-U>Lspsaga range_code_action<CR>
" 悬停文档
nnoremap <silent>K :Lspsaga hover_doc<CR>
" scroll down hover doc or scroll in definition preview
nnoremap <silent> <C-f> <cmd>lua require('lspsaga.action').smart_scroll_with_saga(1)<CR>
" scroll up hover doc
nnoremap <silent> <C-b> <cmd>lua require('lspsaga.action').smart_scroll_with_saga(-1)<CR>
" help
nnoremap <silent> gs :Lspsaga signature_help<CR>
" 重命名
nnoremap <silent>gr :Lspsaga rename<CR>
" 预览定义
nnoremap <silent> gd :Lspsaga preview_definition<CR>
" 浮动终端
nnoremap <silent> <A-d> :Lspsaga open_floaterm<CR>
tnoremap <silent> <A-d> <C-\><C-n>:Lspsaga close_floaterm<CR>
  1. neovim-lsp
  2. nvim-treesitter
  3. nvim-lspconfig
  4. defx.nvim
  5. defx.txt
  6. How to set up Neovim 0.5 + Modern plugins (LSP, Treesitter, Fuzzy finder, etc)
  7. dotfiles-public
  8. programming-go-in-neovim
  9. go-vim
  10. awesome-neovim
  11. VIM USER MANUAL
  12. runtimepath
  13. 如何配置 Vim 的 Golang 开发环境

grpc SSL/TLS

gRPC提供了内置的授权机制(Authorization),也提供接口用于扩展自定义授权验证。gRPC旨在和多种身份验证(Authentication)机制配合使用,可以轻松安全的使用gRPC同其他系统进行通信。

gRPC支持下面几种机制:

  • SSL/TLS:gRPC集成了SSL/TLS,并促进使用SSL/TLS对服务进行身份验证,并对客户端和服务端之间交互的所有数据进行加密
  • ALTS
  • Token-based authentication with Google

同时,也支持扩展自定义认证机制。

如下面一个最基本的Hello grpc程序中, 请求和响应都是明文传输,容易造成敏感信息、伪造篡改等问题。

server.go

package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "xwxwgo.com/hello-grpc/lib/protos"
)
type Server struct {
pb.UnimplementedHelloServiceServer
}
func (s *Server) SayHello(ctx context.Context, req *pb.HelloReq) (*pb.HelloRsp, error) {
return &pb.HelloRsp{Reply: req.Greeting}, nil
}
func main() {
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
server := grpc.NewServer()
pb.RegisterHelloServiceServer(server, &Server{})
server.Serve(lis)
}

client/main.go

package main
import (
"context"
"log"
"google.golang.org/grpc"
pb "xwxwgo.com/hello-grpc/lib/protos"
)
func main() {
conn, err := grpc.Dial(":8080", grpc.WithInsecure())
if err != nil {
log.Fatalf("dail field: %v", err)
}
defer conn.Close()
client := pb.NewHelloServiceClient(conn)
rsp, err := client.SayHello(context.Background(), &pb.HelloReq{Greeting: "Hello grpc"})
if err != nil {
log.Fatalf("call server failed: %v", err)
}
log.Printf("SayHello: %+v", rsp)
}

通过Wireshark本地gRPC抓包,可以看到请求具体信息:

image-20211019163046195

通过SSL/TLS对服务进行身份认证,可以客户端和服务端之间交互的所有数据进行加密。

Terminal window
# 私钥
$ openssl ecparam -genkey -name secp384r1 -out server.key
# 自签公钥
$ openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

Client:

credentials.NewClientTLSFromFile:通过服务端的自签公钥和服务名称,来构造TLS凭证

creds, _ := credentials.NewClientTLSFromFile(certFile, "hello-grpc")
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))
// error handling omitted
client := pb.NewHelloServiceClient(conn)
// ...

Server:

credentials.NewServerTLSFromFile:服务端证书文件和密钥构造 TLS 凭证

creds, _ := credentials.NewServerTLSFromFile(certFile, keyFile)
s := grpc.NewServer(grpc.Creds(creds))
pb.RegisterHelloServiceServer(s, &server.HelloServer{})
lis, _ := net.Listen("tcp", "localhost:50051")
// error handling omitted
s.Serve(lis)

再次进行抓包:

image-20211019164650896

  1. Analyzing gRPC messages using Wireshark
  2. Loopback capture

创建型模式

  • 简单工厂模式(Simple Factory)
  • 单例模式(Singleton)
  • 工厂方法模式(Factory Method)
  • 抽象工厂模式(Abstract Factory)
  • 创建者模式(Builder)
  • 原型模式(Prototype)

Go中没有构造函数,一般都是以NewXxx函数来进行相关初始化工作,当NewXxx函数返回接口时,这就是一个简单工厂模式,包中会封装接口的实现细节。如:

package simple
type API interface {
Do()
}
func NewAPI() API {
return api{}
}
type api struct {
}
func (a api) Do() {
//TBD...
}

使用

var api = simple.NewAPI()
api.DO()

单例模式将类型的实例化限制为单个对象。

package singleton
type singleton map[string]string
var (
once sync.Once
instance singleton
)
func New() singleton {
once.Do(func() {
instance = make(singleton)
})
return instance
}

使用

s := singleton.New()
s["this"] = "that"
s2 := singleton.New()
fmt.Println("This is ", s2["this"])
//This is that

经验:

单例模式表现的是一个全局状态,同时在大多数时候会降低可测试性。

TBD…

TBD…

TBD…

TBD…

设计模式:Options Pattern In Golang

下面将记录函数可选(functional options)模式在golang中的实现。

函数可选参数模式(或者可选参数模式、Optional Parameters Pattern),用在当构造函数和公共函数API需要可选参数,特别时当具有三个或者更多可选参数时。

这个模式的优势在于,可以实现一个方法,并用下面的简单方式进行调用,如:

obj.Method(mandatory1, mandatory2)

或者向下面这样,通过是用可选的参数,来改变它的行为:

obj.Method(mandatory1, mandatory2, option1, option2, option3)

这可以避免为可选参数,使用笨重的零值参数:

obj.Method(mandatory1, mandatory2, nil, "", 0)

或者使用同样笨重的使用配置对象的方式:

cfg := &ConfigForMethod{
Optional1: ...,
Optional2: ...,
Optional3: ...,
}
obj.Method(mandatory1, mandatory2, &cfg)

使用一个Option接口,该接口保存一个未导出的方法,同时在一个未导出options结构中记录可选的参数信息。

package db
type options struct {
cache bool
limit int
logger *zap.Logger
}
type Option interface {
apply(*options)
}
//cache
type cacheOption bool
func (c cacheOption)apply(opts *options){
opts.cache = bool(c)
}
func WithCache(c bool)Option{
return cacheOption(c)
}
//limit
type limitOption int
func (l limitOption)apply(opts *options){
opts.limit = int(l)
}
func WithLimit(limit int)Option{
return limitOption(limit)
}
//logger
type loggerOption struct {
logger *zap.Logger
}
func (l loggerOption)apply(opts *options){
opts.logger = l.logger
}
func WithLogger(log *zap.Logger) Option{
return loggerOption{logger: log}
}

函数:

package db
func Open(addr string, opts ...Option)(*Connection, error){
//默认值
options := options {
cache: 1,
limit: 1,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(options)
}
//...
}

使用:

db.Open(addr)
db.Open(add, db.WithLimit(10))
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(1), db.WithLimit(10), db.WithLogger(log))

将Option定义为函数类型,使用闭包来实现

package db
type Option func(*options)
type options struct {
cache bool
limit int
logger *zap.Logger
}
func WitchCache(cache bool)Option{
return func(opts *options){
opts.cache = cache
}
}
func WithLimit(limit int) Option {
return func(opts *options){
opts.limit = limit
}
}
func WithLogger(log *zap.Logger) Option {
return func(opts *options) {
opts.logger = log
}
}

函数:

package db
func Open(addr string, opts ...Option)(*Connection, error){
//默认值
options := options {
cache: 1,
limit: 1,
logger: zap.NewNop(),
}
for _, o := range opts {
o(options)
}
//...
}

使用

db.Open(addr)
db.Open(add, db.WithLimit(10))
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(1), db.WithLimit(10), db.WithLogger(log))
type dailOption struct {
disableRetry bool
}
type DailOption interface {
apply(*dailOption)
}
type funcDialOption struct {
f func(*dailOption)
}
func (fdo *funcDialOption) apply(do *dailOption) {
fdo.f(do)
}
func newFuncDialOption(f func(o *dailOption)) *funcDialOption {
return &funcDialOption{f: f}
}
func WithDisableRetry() DailOption {
return newFuncDialOption(func(o *dailOption) {
o.disableRetry = true
})
}

函数:

// Dial creates a client connection to the given target.
func Dial(target string, opts ...DialOption) (*ClientConn, error) {
return DialContext(context.Background(), target, opts...)
}

使用:

grpc.Dial("localhost:8080")
grpc.Dial("localhost:8080", grpc.WithInsecure(), grpc.WithBlock())
grpc.Dial("localhost:8080", grpc.WithDisableRetry())

方式3中,需要将具体的配置选项暴露出来,而这种方式不用。

通用方式实现一个可复用组件,来实现一个具有下面形式参数的函数:

obj.Method(mandatory1, mandatory2, option1, option2, option3)

在内部,只需要像下面这样声明定义该方法:

func (obj *Object)Method(m1 Type1, m2 Type2, options ...Option) {
....
}

Option对象有两个部分,一个标识和一个值。标识和值都被声明成interface{},这样标识和值都可以是任意的数据类型。对于标识,通常最好使用一个未导出的空结构,如:

// Interface defines the minimum interface that an option must fulfill
type Option interface {
// Ident returns the "indentity" of this option, a unique identifier that
// can be used to differentiate between options
Ident() interface{}
// Value returns the corresponding value.
Value() interface{}
}
type pair struct {
ident interface{}
value interface{}
}
// New creates a new Option
func New(ident, value interface{}) Option {
return &pair{
ident: ident,
value: value,
}
}
func (p *pair) Ident() interface{} {
return p.ident
}
func (p *pair) Value() interface{} {
return p.value
}
type identOptionalParamOne struct{}
type identOptionalParamTwo struct{}
type identOptionalParamThree struct{}
func WithOptionOne(v ...) Option {
return option.New(identOptionalParamOne{}, v)
}

然后,可以通过下面的方式,调用上面定义的Method方法:

obj.Method(m1, m2, WithOptionOne(...), WithOptionTwo(...), WithOptionThree(...))

同时Method的options参数,需要用类似下面的方式进行解析:

func (obj *Object) Method(m1 Type1, m2 Type2, options ...Option) {
paramOne := defaultValueParamOne
for _, option := range options {
switch option.Ident() {
case identOptionalParamOne{}:
paramOne = option.Value().(...)
}
}
...
}

Apple Login

这里记录了,接入AppleID登录,服务端的实现。

针对后端验证苹果提供了两种验证方式:

  • 一种是 基于JWT的算法验证
  • 一种是 基于授权码的验证

identityToken是一个经过签名的JSON Web Token的方式实现,更多见Sign in with Apple REST API。 另一种方式是校验授权码,其主要分为两步:1. 用户授权后获取code;2. 通过code换取token。

服务端验证identityToken时,App客户端需要将苹果接口返回的identityToken, userID这两个参数传递给服务器,用于验证本次登录的有效性。

服务端,获取到Apple公钥,来验证identityToken的有效性。

公钥获取接口:https://appleid.apple.com/auth/keys , 文档见Fetch Apple’s Public Key for Verifying Token Signature

代码在GitHub

实现1,主要使用github.com/dgrijalva/jwt-go 和 github.com/lestrrat-go/jwx/jwk

func VerfiyToken(token string) error {
_, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
set, err := jwk.Fetch(context.Background(), "https://appleid.apple.com/auth/keys")
if err != nil {
return nil, err
}
kid := t.Header["kid"]
if key, ok := set.LookupKeyID(fmt.Sprint(kid)); ok {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %s", t.Header["alg"])
}
var rawKey interface{}
if err = key.Raw(&rawKey); err != nil {
return nil, err
}
return rawKey, nil
}
return nil, nil
})
return err
}

实现2 github.com/lestrrat-go/jwx/jwk

func VerifyToken2(payload string) error {
set, err := jwk.Fetch(context.Background(), "https://appleid.apple.com/auth/keys")
if err != nil {
return err
}
token, err := ljwt.Parse([]byte(payload), ljwt.WithKeySet(set), ljwt.UseDefaultKey(true))
if err != nil {
return err
}
fmt.Println(token)
return nil
}

依赖注入wire

wire是Google开源的一款代码生成工具,通过依赖注入来自动链接不同的组件。

依赖注入 是用来构建地耦合,灵活易维护地代码的标注技术。在wire中,组件间的依赖关系表示为函数参数(function parameters),通过显示的初始化而不是全局变量。wire没有使用运行时状态或反射来实现依赖注入,而是使用代码生成。

像Wire这样的依赖注入工具旨在简化初始化代码的管理。可以将服务及其依赖关系描述为代码或配置,Wire计算出依赖关系图,并确认如何传递每个服务所需的内容。通过更改函数签名、添加或删除初始化项来更改应用程序的依赖关系,然后Wire为整个依赖关系图生成初始化代码。

Golang社区中其他的依赖注入框架,如来自Uber的dig,Facebook的inject。他们都通过反射机制实现运行时依赖注入。Wire主要灵感来自于Dagger 2,使用代码生成而不是反射或service locators。这样做带来的好处:

  • 当依赖关系更加复杂时,运行时依赖注入会变得难以跟踪和调试。使用代码生成意味着在运行时执行的初始化代码是常规的,惯用的Go代码,易于理解和调试。特别是像忘记依赖之类的问题,将变成编译时错误而不是运行时错误
  • 因为不需要 Service Locators, 所以对命名没有特殊要求
  • 避免依赖膨胀变得更简单。Wire生成的代码仅仅只包含需要的依赖,所以构建出的程序中不会含有没有使用过的依赖。而运行时依赖注入直到执行时,才能识别出没有使用过的依赖
  • 依赖关系静态存于源码之中, 便于工具分析与可视化

Wire的两个核心概念是:providersinjectors

provider是Wire的核心机制,它是一个可以返回一个值的普通Go函数(function),如:

package foobarbaz
type Foo struct {
X int
}
// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
return Foo{X: 42}
}

为了可以在其他包中使用,provider函数必须要可以是可以导出的,同时provider函数可以使用参数来指定依赖:

// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}

provider也可以返回error:

// ProvideBaz returns a value if Bar is not zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
if bar.X == 0 {
return Baz{}, errors.New("cannot provide baz when bar is zero")
}
return Baz{X: bar.X}, nil
}

通过ProviderSet可以将一组provider组合到一起,实践中,通常将一些一起使用的provider放到一起组成一个ProviderSet:

var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)

provider set 可以再放入到另一个provider set 中:

var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)

injector是由Wire工具自动生成的函数,它将按照依赖顺序调用相关的provider。

通过编写一个函数来声明一个injector,在这个函数体中,将provider作为参数调用wire.Build。wire只关注这个定义函数的返回类型,wire在生成代码时会忽略函数的返回值。

在定义injector的函数中

// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import (
"context"
"github.com/google/wire"
"example.com/foobarbaz"
)
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}

在包目录上执行wire命令,生成injector的代码会放到wire_gen.go文件中。如:

// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject
package main
// Injectors from wire.go:
func InitializeEvent() (Event, error) {
message := NewMessage()
greeter := NewGreeter(message)
event, err := NewEvent(greeter)
if err != nil {
return Event{}, err
}
return event, nil
}

Go标准库atomic

在 Go 语言标准库中,sync/atomic包将底层硬件提供的原子操作封装成了 Go 的函数。但这些操作只支持几种基本数据类型,Go 语言在 1.4 版本的时候向sync/atomic包中添加了一个新的类型Value。此类型的值相当于一个容器,可以被用来“原子地”存储(Store)和加载(Load)任意类型的值。

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子性不可能由软件单独保证—必须需要硬件的支持,因此是和架构相关的。在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀”LOCK”,经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。

mutex操作系统实现,而atomic包中的原子操作则由底层硬件直接提供支持。因此原子操作可以在lock-free的情况下保证并发安全,并且它的性能也能做到随 CPU 个数的增多而线性扩展。

atomic提供的函数功能需要非常小心才能正确使用,除了在一些特殊的low-level程序中,同步最好使用通道或sync包的设施来完成。

1.4 版本前,sync/atomic包支持几种基本数据类型的原子操作,之后新增的atomic.Value类型,供一致类型值的原子加载和存储,支持任意类型的数据,内部的字段是一个interface{}类型。

// A Value provides an atomic load and store of a consistently typed value.
// The zero value for a Value returns nil from Load.
// Once Store has been called, a Value must not be copied.
//
// A Value must not be copied after first use.
type Value struct {
v interface{}
}

参见:Go 语言标准库中 atomic.Value 的前世今生

unsafe.Pointer 可以绕过 Go 语言类型系统的检查,完成任何类型与内建的 uintptr 类型之间的转化。unsafe.Pointer 可以实现四种其他类型不能的操作:

  • 任何类型的指针都可以转化为一个 unsafe.Pointer
  • 一个 unsafe.Pointer 可以转化成任何类型的指针
  • 一个 uintptr 可以转化成一个 unsafe.Pointer
  • 一个 unsafe.Pointer 可以转化成一个 uintptr
  1. [atomic doc] https://pkg.go.dev/sync/atomic
  2. [Go 语言标准库中 atomic.Value 的前世今生] https://mp.weixin.qq.com/s?__biz=MzAxMTA4Njc0OQ==&mid=2651445490&idx=2&sn=03e00ae51a511392a9923f6d72349fc9&scene=21#wechat_redirect
  3. [原子操作] https://baike.baidu.com/item/%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C/1880992?fr=aladdin

Use Go Embed

Go1.16引入新的//go:embed指令,可以在编译时嵌入文件和目录,并对其进行访问。通过它,真正做到部署时只有一个二进制文件。

背景:2021-02-16,Go Team正式发布了Go1.16。该版本包含下面的一些重要变化:

  • embed 包和 //go:embed 指令
  • 增加对 macOS ARM64 的支持
  • 默认启用 Module
  • io/fs 包
  • 弃用io/ioutil

最后,还有许多其他改进和错误修复,包括构建速度提高了 20-25%,linux/amd64上内存使用量减少了 5-15%。有关更改的完整列表以及有关上述改进的更多信息,请参阅 Go 1.16 发行说明

基本思路是,在代码中添加特殊注释,Go将知道其包含的一个或多个文件。

Go源文件在引入“embed”包后,可以使用//go:embed指令在编译时,从包目录或者子目录的文件读取内容来初始化

string,[]byte,FS类型的变量。如,可以用下面的三种方式嵌入名为hello.txt文件,然后在运行时打印其内容。

目录结构:

Terminal window
.
├── main.go
└── hello.txt
  • 将文件内容嵌入到一个字符串变量
import (
_ "embed"
"fmt"
)
//go:embed hello.txt
var s string
fmt.Print(s)
  • 将文件内容嵌入到[]byte
import (
_ "embed"
"fmt"
)
//go:embed hello.txt
var b []byte
fmt.Print(string(b))
  • 将一个或多个文件嵌入到文件系统中
import (
_ "embed"
"fmt"
)
//go:embed hello.txt
var f embed.FS
data, _ := f.ReadFile("hello.txt")
fmt.Print(string(data))
package main
import (
_ "embed"
"fmt"
"strings"
)
var (
Version string = strings.TrimSpace(version)
//go:embed version.txt
version string
)
func main() {
fmt.Printf("Version %q\n", Version)
}

对于更复杂的示例,我们甚至可以根据是否将某个构建标记传递给go工具来有条件地包含版本信息。

version_dev.go
// +build !prod
package main
var version string = "dev"
version_prod.go
// +build prod
package main
import (
_ "embed"
)
//go:embed version.txt
var version string
Terminal window
$ go run .
Version "dev"
$ go run -tags prod .
Version "0.0.1"
package main
import (
_ "embed"
"fmt"
)
//go:embed quine.go
var src string
func main() {
fmt.Print(src)
}

当运行时,就可以将自己打印出来。

可以网站所需要的所有静态文件或者模板包含在一个可执行文件中,甚至可以通过命令行参数,在读取磁盘文件和读取嵌入文件之间进行切换。

package main
import (
"embed"
"io/fs"
"log"
"net/http"
"os"
)
func main() {
useOS := len(os.Args) > 1 && os.Args[1] == "live"
http.Handle("/", http.FileServer(getFileSystem(useOS)))
http.ListenAndServe(":8888", nil)
}
//go:embed static
var embededFiles embed.FS
func getFileSystem(useOS bool) http.FileSystem {
if useOS {
log.Print("using live mode")
return http.FS(os.DirFS("static"))
}
log.Print("using embed mode")
fsys, err := fs.Sub(embededFiles, "static")
if err != nil {
panic(err)
}
return http.FS(fsys)
}

关于嵌入有些地方需要注意,首先必须要将包导入到任何使用embed命令的文件中。如没有导入包时:

package main
import (
"fmt"
)
//go:embed file.txt
var s string
func main() {
fmt.Print(s)
}
$ go run mian.go
main.go:7:3: //go:embed only allowed in Go files that import "embed"

其次,embed命令只能在包级别变量使用,不能用在函数或方法中。

更多:使用规则,详见文档:https://golang.org/pkg/embed/

相关代码在https://github.com/bytedaring/embed

  1. Go 1.16 Release Notes
  2. embed Doc
  3. How to Use //go:embed
  4. Managing Go installations
  5. 来了来了!Go1.16 重磅发布

GRPC系列 简述

gRPC是可以在任何环境中运行的现代开源高性能RPC框架。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。

图1

  • 在微服务风格架构中有效连接多种服务
  • 将移动设备,浏览器客户端连接到后端服务
  • 生效高效的客户端
  • 十种常用语言的客户端库
  • 高效的连接和简单的服务定义框架
  • 双向流和基于http/2的传输
  • 可插拔身份验证,跟踪,负债均衡和运行状况检查

gRPC默认使用Protocol Buffers序列化协议,用于序列化结构化数据。当然也可以使用json。

定义一个服务, 指定其可以被远程调用的方法及其参数和返回类型。gRPC 默认使用 protocol buffers 作为接口定义语言,来描述服务接口和有效载荷消息结构。

service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
required string greeting = 1;
}
message HelloResponse {
required string reply = 1;
}

gRPC支持的四种服务方法:

客户端发送一个请求给服务端,从服务端获取一个应答,就像一次简单的服务调用。

rpc SayHello(HelloRequest) returns (HelloResponse){
}

5.2、服务端流式RPC(Server-side streaming RPC

Section titled “5.2、服务端流式RPC(Server-side streaming RPC)”

客户端发送一个请求给服务端,可获得一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。gRPC保证单个RPC调用中的消息顺序。

rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}

5.3、客户端流式RPC**(Client-side streaming RPC)**

Section titled “5.3、客户端流式RPC**(Client-side streaming RPC)**”

客户端用提供的一个数据流写人并发送一系列消息给服务端。一旦客户端完成消息写入,就等服务端读取这些消息并返回应答。gRPC保证单个RPC调用中的消息顺序。

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}

5.4、双向流式RPC**(Bidirectional streaming RPC)**

Section titled “5.4、双向流式RPC**(Bidirectional streaming RPC)**”

双方都使用读写流发送一系列消息。这两个数据流是独立的,所以服务端和客户端可以按其期望的任意顺序读写,如:服务端可以在写响应之前等待接收所有客户端消息,或者可以先读消息再写消息,或者其他一些读写组合。每个流中的消息顺序都会保留。

rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}

gRPC提供protocol buffer 编译插件,能够从一个定义服务的.proto文件生成客户端和服务端代码。通常gRPC用户可以在服务端实现这些API,并从客户端调用:

  • 在服务端,服务器实现服务声明的方法,并运行gRPC服务器处理客户端调用。gRPC底层架构会解码传入的请求,执行服务方法,编码服务应答。
  • 在客户端,客户端有一个存根实现了服务端相同的方法。客户端可以在本地存根调用这些方法,用合适的protocol buffer消息类型封装这些参数,gRPC来负债发送请求给服务端并返回服务端protocol buff响应。s

gRPC系列-开端

gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.

gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。

过去一段时间,基于gRPC框架进行开发后端服务接口。接下来,将计划一个系列,记录和继续深入研究和更好的使用gRPC。

Http和golang

证书可以自签证书,也可以从CA购买证书,或者使用Let’s Encrypt的免费证书。

package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello world")
})
log.Fatal(http.ListenAndServeTLS(":8080", "./defaultCA.pem", "./defaultCA.key", nil))
}
Terminal window
#生成服务端私钥
openssl genrsa -out defaultCA.key 2048
#基于私钥生成自签名(x509)证书(.pem|.crt)
openssl req -x509 -new -nodes -key defaultCA.key -days 1024 -out defaultCA.pem

Let’s Encrypt是一个提供免费证书的非盈利机构,并且提供HTTP API来获取证书。

golang.org/x/crypto/acme/autocert包可以自动从Let’s Encrypt或者其他任何基于ACME CA获取证书。

Context包

在Go 1.7中,context包被引入到标准库中。 context是处理并发问题的一个标准风格。

context包内容很简单:

var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
type CancelFunc func()
type Context interface{ ... }
func Background() Context
func TODO() Context
func WithValue(parent Context, key, val interface{}) Context

在使用context包的过程中,不应该Context存储在struct中,而是将Context作为参数传递给需要它的函数。不能传递nil Context,如果不确定该使用什么样的Context,那么传递context.TODO。

Golang和假共享(false sharing)

多核处理器(SMP)系统中, 每一个处理器都有一个本地高速缓存。内存系统必须保证高速缓存的一致性。当不同处理器上的线程修改驻留再同一高速缓存中的变量时就会发生假共享(false sharing),结果导致高速缓存无效,并强制更新,进而影响系统性能。

假共享是 SMP 系统上的一种常见性能问题。在SMP系统中,每个处理器均有一个高速缓存。 当不同处理器上的线程修改驻留在同一高速缓存行(Cache Block,或Cache Line)中的变量时就会发生假共享。 这种现象之所以被称为假共享,是因为每个线程并非真正共享相同变量的访问权。 访问同一变量或真正共享要求编程式同步结构,以确保有序的数据访问。

img

线程 0 和线程 1 会用到不同变量,它们在内存中彼此相邻,并驻留在同一高速缓存块(Cache Block,或Cache Line)。 高速缓存行被加载到 CPU 0 和 CPU 1 的高速缓存中(灰色箭头)。 尽管这些线程修改的是不同变量(红色和蓝色箭头),高速缓存行(Cache Block,或Cache Line)仍会无效,并强制内存更新以维持高速缓存的一致性,这会降低应用性能。

避免假共享的主要方式是进行代码检查。 潜在的假共享主要出现在线程访问全局或动态分配共享数据结构。 注意,在线程访问内存中碰巧相近的几个完全不同的全局变量时,也会出现假共享。 线程本地存储或本地变量不会导致假共享。

可以通过内存填充(padding)的方式予以更正, 目的是确保引起假共享的变量在内存中存放的位置相隔足够远,从而不会驻留在同一高速缓存块中。

来自intel开发社区的避免共享的建议

避免假共享,但要谨慎使用。 过度使用会影响处理器可用高速缓存的有效使用。 即便对于多处理器共享高速缓存设计,仍然建议避免假共享。 尝试最大限度提高多处理器共享高速缓存的利用率,可能会带来一些好处,但一般不会超过支持不同高速缓存架构的多代码路径,所需的软件维护成本。

现在基本上所有CPU的缓存行大小都是64字节,如下面的Go代码, 数据可能会驻留在同一高速缓存中,进而出现假共享。

代码来自colobu

type NoPad struct {
a uint64
b uint64
c uint64
}
func (np *NoPad) Increase() {
atomic.AddUint64(&np.a, 1)
atomic.AddUint64(&np.b, 1)
atomic.AddUint64(&np.c, 1)
}

可以通过填充64字节,使abc不会驻留同一高速缓存中,进而避免伪共享:

type Pad struct {
a uint64
_p1 [8]uint64
b uint64
_p2 [8]uint64
c uint64
_p3 [8]uint64
}
func (np *Pad) Increase() {
atomic.AddUint64(&np.a, 1)
atomic.AddUint64(&np.b, 1)
atomic.AddUint64(&np.c, 1)
}

性能测试:

func BenchmarkPad_Increase(b *testing.B) {
pad := &Pad{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
pad.Increase()
}
})
}
func BenchmarkNoPad_Increase(b *testing.B) {
nopad := &NoPad{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
nopad.Increase()
}
})
}

运行结果:

Terminal window
goos: darwin
goarch: amd64
BenchmarkPad_Increase-12 50000000 26.2 ns/op 0 B/op 0 allocs/op
BenchmarkNoPad_Increase-12 30000000 47.5 ns/op 0 B/op 0 allocs/op

通过填充后的性能相比于没有填充之前要好很多。

运行时检测方法是使用英特尔® VTune 性能分析器或 英特尔® 性能调优实用程序(英特尔 PTU,请见 /zh-cn/articles/intel-performance-tuning-utility/)。 此方法通过基于事件取样(可发现哪些位置存在高速缓存行共享)来揭示性能影响。 但是,这种影响不区分真共享与假共享。

  1. C++ 和 false sharing
  2. Wiki False sharing
  3. Analysis of False Cache Line Sharing Effects on Multicore CPUs

摘抄自维基百科的CPU缓存介绍:

计算机系统中,CPU高速缓存(英语:CPU Cache,在本文中简称缓存)是用于减少处理器访问内存所需平均时间的部件。在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器。其容量远小于内存,但速度却可以接近处理器的频率。

当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。

缓存之所以有效,主要是因为程序运行时对内存的访问呈现局部性(Locality)特征。这种局部性既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality)。有效利用这种局部性,缓存可以达到极高的命中率。

在处理器看来,缓存是一个透明部件。因此,程序员通常无法直接干预对缓存的操作。但是,确实可以根据缓存的特点对程序代码实施特定优化,从而更好地利用缓存。

结构上,一个直接映射(Direct Mapped)缓存由若干缓存块(Cache Block,或Cache Line)构成。每个缓存块存储具有连续内存地址的若干个存储单元。在32位计算机上这通常是一个双(dword),即四个字节。因此,每个双字具有唯一的块内偏移量。

每个缓存块有一个索引(Index),它一般是内存地址的低端部分,但不含块内偏移和字节偏移所占的最低若干位。一个数据总量为4KB、缓存块大小为16B的直接映射缓存一共有256个缓存块,其索引范围为0到255。使用一个简单的移位函数,就可以求得任意内存地址对应的缓存块的索引。由于这是一种多对一映射,必须在存储一段数据的同时标示出这些数据在内存中的确切位置。所以每个缓存块都配有一个标签(Tag)。拼接标签值和此缓存块的索引,即可求得缓存块的内存地址。如果再加上块内偏移,就能得出任意一块数据的对应内存地址。

CPU缓存_00缓存段结构

此外,每个缓存块还可对应若干标志位,包括有效位(valid bit)、脏位(dirty bit)、使用位(use bit)等。这些位在保证正确性、排除冲突、优化性能等方面起着重要作用。

在科研领域,C. J. Conti等人于1968年在描述360/85和360/91系统性能差异时最早引入了高速缓存(cache)一词[8]。Alan Jay Smith于1982年的一篇论文中引入了空间局部性和时间局部性的概念。Mark Hill在1987年发明了3C(Compulsory, Capacity, Conflict)冲突分类。[9]

最早介绍非阻塞缓存的论文之一来自David Kroft(1981年)。1990年Norman Paul Jouppi在一篇论文中介绍了受害者缓存并研究了使用流缓冲器进行预取的性能。[10]

在工业领域,最早的有记载的缓存出现在IBM360/85系统上[11]

Intelx86架构CPU从386开始引入使用SRAM技术的主板缓存,大小从16KB到64KB不等。486引入两级缓存。其中8KBL1缓存和CPU同片,而L2缓存仍然位于主板上,大小可达268KB。将二级缓存置于主板上在此后十余年间一直设计主流。但是由于SDRAM技术的引入,以及CPU主频和主板总线频率的差异不断拉大,主板缓存在速度上的对内存优势不断缩水。因此,从Pentium Pro起,二级缓存开始和处理器一起封装,频率亦与CPU相同(称为全速二级缓存)或为CPU主频的一半(称为半速二级缓存)。

AMD则从K6-III开始引入三级缓存。基于Socket 7接口的K6-III拥有64KB和256KB的同片封装两级缓存,以及可达2MB的三级主板缓存。

今天的CPU将三级缓存全部集成到CPU芯片上。多核CPU通常为每个核配有独享的一级和二级缓存,以及各核之间共享的三级缓存。

  1. https://medium.com/@genchilu/whats-false-sharing-and-how-to-solve-it-using-golang-as-example-ef978a305e10
  2. https://software.intel.com/zh-cn/articles/avoiding-and-identifying-false-sharing-among-threads
  3. https://colobu.com/2019/01/24/cacheline-affects-performance-in-go/
  4. https://zh.wikipedia.org/wiki/CPU%E7%BC%93%E5%AD%98
  5. https://github.com/genchilu/concurrencyPractice/tree/master/golang/pad
  6. https://stackoverflow.com/questions/14707803/line-size-of-l1-and-l2-caches
  7. https://www.scss.tcd.ie/Jeremy.Jones/CS3021/5%20caches.pdf
  8. https://www.7-cpu.com

Golang排序

官方package: sort, 提供了对int,float,string的排序

  • sort.Ints
  • sort.Floats
  • sort.Strings
ints := []int{5, 6, 1, 2, 3} // unsorted
sort.Ints(ints)
fmt.Println(ints)
sort.Sort(sort.Reverse(sort.IntSlice(ints))) // sorted descending
fmt.Println(ints)
floats := []float64{1.3, 3.2, 2.2} // unsorted
sort.Float64s(floats) // sorted ascending
fmt.Println(floats)
sort.Sort(sort.Reverse(sort.Float64Slice(floats))) // sorted descending
fmt.Println(floats)
strings := []string{"b", "a", "b1", "1"} // unsorted
sort.Strings(strings)
fmt.Println(strings)
sort.Sort(sort.Reverse(sort.StringSlice(strings))) // sorted descending
fmt.Println(strings)
func SliceStable(slice interface{}, less func(i, j int) bool)
func Slice(slice interface{}, less func(i, j int) bool)

sort.Slice使用function less(i, j int) bool 来对slice进行排序。

sort.SclieStabl使用function less(i, j int)bool对slice进行排序,当遇到遇到相等的元素时,将会保持原来的顺序不变。

people := []struct {
Name string
Age int
}{
{"Alice", 25},
{"Elizabeth", 75},
{"Alice", 75},
{"Bob", 75},
{"Alice", 75},
{"Bob", 25},
{"Colin", 25},
{"Elizabeth", 25},
}
// Sort by name, preserving original order
sort.SliceStable(people, func(i, j int) bool { return people[i].Name < people[j].Name })
fmt.Println("By name:", people)
// Sort by age preserving name order
sort.SliceStable(people, func(i, j int) bool { return people[i].Age < people[j].Age })
fmt.Println("By age,name:", people)
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}

如:

type Person struct {
Name string
Age int
}
// ByAge implements sort.Interface based on the Age field.
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func main() {
family := []Person{
{"Alice", 23},
{"Eve", 2},
{"Bob", 25},
}
sort.Sort(ByAge(family))
fmt.Println(family) // [{Eve 2} {Alice 23} {Bob 25}]
}

更多:

https://golang.org/pkg/sort/

[Go]Deferred函数案例

package main
func main() {
println(DeferFunc1(1))
println(DeferFunc2(1))
println(DeferFunc3(1))
}
func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}
func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}
func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}

运行结果:

4
1
3

解析:

首先,只有在函数执行完毕后,这些被延迟的函数才会执行;其次,defer语句中的函数会在return语句更新返回值变量后再执行,而且函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量。所以,DeferFunc1, DeferFunc3,分别返回4,3。DeferFunc2中因为defer中的匿名函数更新的是函数中变量t,不会影响返回值所以返回1。

使用Homebrew安装Golang

安装步骤,参考Homebew

Terminal window
$ brew install go
$ brew info go
go: stable 1.9.2 (bottled), devel 1.10beta1, HEAD
Open source programming language to build simple/reliable/efficient software
https://golang.org
/usr/local/Cellar/go/1.8.3 (7,035 files, 282.0MB)
Poured from bottle on 2017-09-05 at 09:29:02
/usr/local/Cellar/go/1.9 (7,639 files, 293.7MB)
Poured from bottle on 2017-09-05 at 09:45:50
/usr/local/Cellar/go/1.9.2 (7,646 files, 293.9MB) *
Poured from bottle on 2018-01-09 at 16:50:46
From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/go.rb
==> Requirements
Build: git ✔
Required: macOS >= 10.8 ✔
==> Options
--without-cgo
Build without cgo (also disables race detector)
--without-race
Build without race detector
--devel
Install development version 1.10beta1
--HEAD
Install HEAD version
==> Caveats
A valid GOPATH is required to use the `go get` command.
If $GOPATH is not specified, $HOME/go will be used by default:
https://golang.org/doc/code.html#GOPATH
You may wish to add the GOROOT-based install location to your PATH:
export PATH=$PATH:/usr/local/opt/go/libexec/bin

编辑配置文件~/.zshrc, 更新内容:

export GOPATH=/Users/xiaoxiwang/go:/Users/xiaoxiwang/Documents/demo/golangDemo
export GOROOT=/usr/local/opt/go/libexec
export PATH=$PATH:/Users/xiaoxiwang/anaconda3/bin:/usr/local/opt/go/libexec/bin

保存修改,更新配置

source ~/.zshrc

2.4、安装完成

查看安装的golang信息:

$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/xiaoxiwang/go:/Users/xiaoxiwang/Documents/demo/golangDemo"
GORACE=""
GOROOT="/usr/local/opt/go/libexec"
GOTOOLDIR="/usr/local/opt/go/libexec/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/2s/5xlkp2f149j5bydw1rhxhyfw0000gn/T/go-build390073932=/tmp/go-build -gno-record-gcc-switches -fno-common"
CXX="clang++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"

使用Golang基于MongoDB构建Restful服务

近来使用Golang来构建Restful服务变得越发流行。我发现有些时候使用MongoDB作为持久存储,这篇文档中, 我会使用Golang和MongoDB来构建一个简单的用户管理为服务。

MongoDB因为极简、灵活、高可用以及面向文档的特性得到越来越多市场上的青睐。根据MongoDB之父的解释,它被用来设计组合键值对存储和关系数据库存储的最佳特性。MongoDB在两者之间做妥协,具备了二者的某些有用的功能。

MongoDB的应用场景:Web应用、分析应用的首要数据库,以及弱数据类型的数据,也就是无schema数据。

文档就是键值对集合。文档中的键用字符串表示,文档中的值可以是基础的数据类型(字符串、数字、日期等)、数组,也可以是另一个文档。在MongDB内部以二进制JSON格式存储文档数据,也就做BSON。BSON有相似的结构,但专为文档存储而设计。

下面是一个文档数据示例:

{
name: '张三',
age: '11',
address: '湖北省武汉市光谷一路'
}

集合是结构或者概念上相似文档的容器。例如,我们会把用户(user)文档存储到(users)集合(collection)中。这里集合的概念就非常类似于关系数据库(RDMS)中表(table)的概念。两者的不同是,集合中的数据是无schema的,是不强制数据结构的,可以是任意的。

MongoDB不是用SQL,而是使用自己的JSON查询语言。

例如:使用SQL语句查询名叫“张三”的用户

SELECT * from users
WHERE name = '张三'

而在MongoDB中,查询的是:

db.users.find({name: 'hello'})

mgo(发音:mango)是一个Go语言实现的MongoDB驱动程序,这个驱动提供了一个非常简洁易于使用、并经过充分测试API。接下来,在介绍如何通过mgo来实现CRUD(create、react、update、delete)操作之前,将简单介绍下会话管理(session manager)。

获取会话

session, err := mgo.Dial("localhsot")

单个的会话不允许进行并发处理,所以通常需要使用多个会话。新建一个会话的最快方式是从现有的session中复制一个新的会话:

newSession := session.Copy()
defer newSession.Close()

新生成的这个会话会使用相同的集群信息和连接池(connection pool)。每一个新建的session必须在生命周期结束时调用Close方法,该会话的资源会视情况而定,是被放回连接池,还是被回收。

mgo需要和bson一同使用,bson使编写查询更加简单。

  • 获取集合中所有的文档
c := session.DB("store").C("users")
var users []User
err := c.Find(bson.M{}).All(&books)
  • 查询单个文档
c := session.DB("store").C("users")
var user User
err := c.Find(bson.M{"name": "张三"}).One(&user)
  • 新建文档
c := session.DB("store").C("users")
err = c.Insert(&User{"Ale"})
  • 更新文档
c := session.DB("store").C("users")
err = c.Update(bson.M{"name": "张三"}, &book)
  • 删除文档
c := session.DB("store").C("users")
err = c.Remove(bson.M{"name": "张三"})

Echo是一个高性能、极简的Go语言Web框架。

  • 优化的 HTTP 路由。
  • 创建可靠并可伸缩的RESTful API。
  • 基于标准的HTTP服务器。
  • 组 APIs.
  • 可扩展的middleware框架。
  • Define middleware at root, group or route level.
  • 为JSON, XML进行数据绑定,产生负荷。
  • 提供便捷的方法来发送各种HTTP相应。
  • 对HTTP错误进行集中处理。
  • Template rendering with any template engine.
  • 定义属于你的日志格式。
  • 高度个性化。
  • Automatic TLS via Let’s Encrypt
  • 支持HTTP/2

performance.png

具体实现中基于Echo框架来开发,代码在github.com

package main
import (
"log"
"net/http"
"fmt"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
mgo "gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
type User struct {
ID string `json:"id" bson:"_id,omitempty"`
Name string `json:"name,omitempty"`
Phone string `json:"phone,omitempty"`
Age int `json:"age,omitempty"`
}
var session *mgo.Session
func init() {
s, err := mgo.Dial("localhost")
if err != nil {
log.Fatal(err)
}
session = s
}
func main() {
defer session.Close()
ensureIndex(session)
session.SetMode(mgo.Monotonic, true)
e := echo.New()
e.Use(middleware.Logger())
e.GET("/users", allUsers)
e.GET("/user/:id", getUser)
e.PUT("/user", updateUser)
e.DELETE("/user/:id", deleteUser)
e.POST("/user", saveUser)
e.Logger.Fatal(e.Start(":1424"))
}
func ensureIndex(s *mgo.Session) {
session := s.Copy()
defer session.Close()
c := session.DB("store").C("users")
index := mgo.Index{
Key: []string{"id"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
err := c.EnsureIndex(index)
if err != nil {
panic(err)
}
}
func saveUser(e echo.Context) error {
u := new(User)
if err := e.Bind(u); err != nil {
return e.JSON(http.StatusBadRequest, err)
}
s := session.Copy()
defer s.Close()
c := s.DB("store").C("users")
err := c.Insert(u)
if err != nil {
log.Println("Failed insert user", u)
if mgo.IsDup(err) {
return e.JSON(http.StatusBadRequest, "User with this id alread exists.")
}
return e.JSON(http.StatusInternalServerError, "Database error")
}
return e.JSON(http.StatusCreated, "SUCCESS")
}
func getUser(e echo.Context) error {
s := session.Copy()
defer s.Clone()
c := s.DB("store").C("users")
var u User
id := e.Param("id")
fmt.Println("userid", id)
err := c.Find(bson.M{"_id": id}).One(&u)
if err != nil {
log.Println("Failed get user", err)
return e.JSON(http.StatusNotFound, "Database error")
}
return e.JSON(http.StatusOK, u)
}
func updateUser(e echo.Context) error {
u := new(User)
if err := e.Bind(u); err != nil {
return e.JSON(http.StatusBadRequest, err)
}
s := session.Copy()
defer s.Close()
c := s.DB("store").C("users")
err := c.Update(bson.M{"_id": u.ID}, &u)
if err != nil {
switch err {
default:
log.Fatalln("Failed update user: ", err)
return e.JSON(http.StatusInternalServerError, "Database error")
case mgo.ErrNotFound:
return e.JSON(http.StatusNotFound, "Not found")
}
}
return e.JSON(http.StatusOK, u)
}
func deleteUser(e echo.Context) error {
s := session.Copy()
defer s.Close()
id := e.Param("id")
c := s.DB("store").C("users")
err := c.Remove(bson.M{"_id": id})
if err != nil {
switch err {
default:
e.JSON(http.StatusInternalServerError, "Database error")
log.Fatalln("Failed delete user: ", err)
return err
case mgo.ErrNotFound:
e.JSON(http.StatusInternalServerError, "User not found")
return err
}
}
return e.JSON(http.StatusOK, "Sucess")
}
func allUsers(e echo.Context) error {
s := session.Copy()
defer s.Close()
c := s.DB("store").C("users")
var users []User
err := c.Find(bson.M{}).All(&users)
if err != nil {
e.JSON(http.StatusInternalServerError, "Database Error")
return err
}
return e.JSON(http.StatusOK, users)
}

curl对于构建和测试RESTful服务来说是一个非常好用的工具,在其他RESTful 服务API的文档中,常常可以看到curl的身影,这里也不例外。

  • 请求
Terminal window
curl -X POST -H 'Content-Type: application/json' -d @body.json http://localhsot:1424/user
body.json
{
"id": "5",
"name": "李四",
"age": 11
}
  • 响应
Terminal window
SUCCESS
  • 请求
Terminal window
curl -X PUT -H 'Content-Type: application/json' -d @body.json http://localhost:1424/user
body.json
{
"id": "1",
"title": "天一",
"age": "-1"
}
  • 响应
Terminal window
{"id":"1","name":"天一","age":-1}
  • 请求

使用python -m json.tool将服务返回的json,进行格式化处理。

Terminal window
curl http://localhost:1424/users | python -m json.tool
  • 响应
Terminal window
[
{
"id": "YE/\ufffd\ufffdDj\ufffd\ufffd\u0004\ufffd-",
"name": "xiwang"
},
{
"id": "2",
"name": "1"
},
{
"age": -1,
"id": "1",
"name": "\u5929\u4e00"
},
{
"id": "YE7\u001d\ufffdDj\ufffd\ufffd\u0004\ufffd.",
"name": "bug"
}
]
  • 请求
Terminal window
curl http://localhost:1424/user/1
  • 响应
{"id":"1","name":"天一","age":-1}
  • 请求
Terminal window
curl -X DELETE http://localhost:1424/user/1
  • 响应
Terminal window
SUCCESS

[转] Go Slice 秘籍

Golang官方总结: Slice Tricks

由于引入了内建的append的方法, 包container/vector的很多方法都被移除,可以被内建的appendcopy方法代替。

下面是栈vector的操作方法的实现,使用slice实现相关的操作。

a = append(a, b...)
b = make([]T, len(a))
copy(b, a)
//如果a不为空, 等效实现
b = append([]T(nil), a...)
a = append(a[:i], a[j:]...)
a = append(a[:i], a[i + 1]...)
// 或者
a = a[:i + copy(a[i:], a[i + 1])]
a[i] = a[len(a) - 1]
a = a[:len(a) - 1]

注意:如果需要被GC回收的元素是一个指针,或者struct含有指针字段,上面的cut,delete实现可能就导致内存泄漏:一些元素的值会被a一直引用而不会被回收。下面的实现可以解决这个问题:

copy(a[i:], a[j:])
for k,n := len(a)-j+i, len(a); k<n; k++ {
a[k] = nil
}
a = a[:len(a)-j+i]
copy(a[i:], a[i+1:])
a[len(a)-1] = nil
a = a[:len(a)-1]
a[i] =a[len(a)-1]
a[len(a)-1]=nil
a = a[:len(a)-1]
a = append(a[:i], append(make([]T, j), a[i:]...)...)
a = append(a, make([]T, j)...)
a = append(a[:i], append([]T{x}, a[i:]...)...)

注意 : 第二个append使用自己的底层存储创建一个新的slice,然后复制a[:i]中的元素到这个slice中,然后再把这些元素复制回a。新slice的创建和第二次的复制通过另外一种方式避免:

s = append(s, 0)
copy(s[i+1:], s[i:])
s[i] = x
a = append(a[:i], append(b, a[i:]...)...)
x, a = a[len(a)-1], a[:len(a)-1]
a = append(a, x)
x, a := a[0], a[1:]
a = append([]T{x}, a...)

这个技巧利用是: slice共享底层的array和存储容量。所以,在过滤slice时,会重用底层的存储。与此同时,底层存储的数据必然会被修改。

b := a[:0]
for _, x := range a {
if f(x) {
b = append(b, x)
}
}

使用相同的元素(不分配新的对象)替换slice中内容,并将其反序。

for i:= len(a)/2 - 1; i >= 0; i--{
opp := len(a)-1-i
a[i], a[opp] = a[opp], a[i]
}

下面的代码实现同样的效果,只不过使用了两个索引变量

for left, right := 0, len(a)-1; left < right; left, right = left + 1, right -1 {
a[left] , r[right] = a[right], a[left]
}