#96 数组按行访问和按列访问速度有这么大差异?
Clang Golang 2023-12-01看到一片技术文章《改了一行代码,数组遍历耗时从 10.3 秒降到了 0.5 秒!》,试验了一下。
coding in a complicated world
看到一片技术文章《改了一行代码,数组遍历耗时从 10.3 秒降到了 0.5 秒!》,试验了一下。
Golang Configuration tool that support YAML, JSON, TOML, Shell Environment (Supports Go 1.10+)
for
语义变更
仔细观察下面的例子就能知道问题在哪里了:
package main
import "fmt"
func main() {
items := []int{1, 2, 3}
{
var all []*int
for _, item := range items {
all = append(all, &item)
}
fmt.Printf("%+v\n", all)
// [0xc00008c018 0xc00008c018 0xc00008c018]
// 输出的都是最后一个值!!!
for _, i := range all {
fmt.Printf("%+v, %+v\n", i, *i)
}
}
// fix it:
{
var all []*int
for _, item := range items {
item := item // 重点
all = append(all, &item)
}
for _, i := range all {
fmt.Printf("%+v, %+v\n", i, *i)
}
}
}
package main
import "fmt"
func main() {
{
var prints []func()
for _, v := range []int{1, 2, 3} {
prints = append(prints, func() { fmt.Println(v) })
}
for _, print := range prints {
print()
}
}
// 输出的是 3 3 3,而不是 1,2,3,Why?
// fix it:
{
var prints []func()
for _, v := range []int{1, 2, 3} {
v := v // 重点
prints = append(prints, func() { fmt.Println(v) })
}
for _, print := range prints {
print()
}
}
}
package main
import (
"fmt"
"sync"
)
func main() {
items := []int{1, 2, 3}
{
wg := sync.WaitGroup{}
for _, v := range items {
wg.Add(1)
go func() {
// 会提示:loop variable v captured by func literal
fmt.Println(v)
wg.Done()
}()
}
wg.Wait()
}
// fix it:
{
wg := sync.WaitGroup{}
for _, v := range items {
wg.Add(1)
v := v // 重点
go func() {
fmt.Println(v)
wg.Done()
}()
}
wg.Wait()
}
}
这个例子可以改写成:
package main
import (
"fmt"
)
func main() {
items := []int{1, 2, 3}
done := make(chan bool)
{
for _, v := range items {
go func() {
// 会提示:loop variable v captured by func literal
fmt.Println(v)
done <- true
}()
}
for _ = range items {
<-done
}
}
// fix it:
{
for _, v := range items {
v := v // 重点
go func() {
fmt.Println(v)
done <- true
}()
}
for _ = range items {
<-done
}
}
}
根据 Go 的设计思想,花括号内是一个独立的作用域,for 循环每一次也应该是独立的。
当前这次循环中的变量和上一次循环的变量应该是不一样的。
但实际上,根据运行结果来看,他们的地址是一样的。
闭包函数应该也是这样的,去原来的位置读相关变量,但是之前的位置写入了新的值。
这个设计是一个大坑,对于新人非常不友好。
从 1.21 开始支持根据环境变量 GOEXPERIMENT=loopvar
来启用新的订正版语义。
从 1.22 开始正式修改 for 语义。
Russ Cox 认为当前语义的代价很大,出错的频率高于正确的频率。
但是这次变更 for 语句语义的决定和之前的承诺有冲突(保证兼容性的基础就是语义不会被重新定义)。
但是好在 Go 已经为这种情况做好了足够的准备,即,go 支持在同一批编译中,按照不同包的 mod 文件中声明的 go 版本来对体现不同的语法特性。
就比如 A 包需要 1.22,然后依赖 B 包,需要 1.18,那么 A 包中的代码按照新的定义来,B 包中的代码按照旧的定义来。
gotip 是官方推出的,从最新的开发分支下拉代码,编译生成 go 运行时的工具。
如果希望体验最新特性,可以在编译成功之后,可以直接用 gotip 取代 go 命令用来执行 go 程序。
gotip 就可以理解为开发版的 go。
go install golang.org/dl/gotip@latest
gotip
gotip: not downloaded. Run 'gotip download' to install to C:\Users\nosch\sdk\gotip
gotip download
Cloning into 'C:\Users\nosch\sdk\gotip'...
...
Building Go cmd/dist using C:\Program Files\Go. (go1.20.5 windows/amd64)
Building Go toolchain1 using C:\Program Files\Go.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for windows/amd64.
成功的时候:
---
Installed Go for windows/amd64 in C:\Users\nosch\sdk\gotip
Installed commands in C:\Users\nosch\sdk\gotip
Success. You may now run 'gotip'!
失败的时候:
# runtime/cgo
gcc_libinit_windows.c: In function '_cgo_beginthread':
gcc_libinit_windows.c:143:27: error: implicit declaration of function '_beginthread'; did you mean '_cgo_beginthread'? [-Werror=implicit-function-declaration]
143 | thandle = _beginthread(func, 0, arg);
| ^~~~~~~~~~~~
| _cgo_beginthread
cc1: all warnings being treated as errors
go tool dist: FAILED: C:\Users\nosch\sdk\gotip\pkg\tool\windows_amd64\go_bootstrap install std: exit status 1
Success. You may now run 'gotip'!
Go 并没有支持集合类型,我们需要自己实现:
https://go.dev/play/p/uVDCiN4Cbpt
package main
import "fmt"
type Set map[string]bool
func (s Set) Add(item string) {
s[item] = true
}
func (s Set) Remove(item string) {
delete(s, item)
}
func (s Set) Contains(item string) bool {
_, exists := s[item]
return exists
}
func main() {
mySet := make(Set)
mySet.Add("apple")
mySet.Add("banana")
mySet.Add("orange")
for item := range mySet {
fmt.Println(item)
}
fmt.Println(mySet.Contains("apple")) // 输出: true
fmt.Println(mySet.Contains("grape")) // 输出: false
mySet.Remove("banana")
fmt.Println(mySet.Contains("banana")) // 输出: false
}
注意:
参考 https://github.com/deckarep/golang-set
的设计:
https://go.dev/play/p/BKWT84lXfuz
package main
import "fmt"
type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(item T) {
s[item] = struct{}{}
}
func (s Set[T]) Remove(item T) {
delete(s, item)
}
func (s Set[T]) Contains(item T) bool {
_, exists := s[item]
return exists
}
func main() {
mySet := make(Set[string])
mySet.Add("apple")
mySet.Add("banana")
mySet.Add("orange")
for item := range mySet {
fmt.Println(item)
}
fmt.Println(mySet.Contains("apple")) // 输出: true
fmt.Println(mySet.Contains("grape")) // 输出: false
mySet.Remove("banana")
fmt.Println(mySet.Contains("banana")) // 输出: false
}
优化点:
看到微信公众号 Go语言教程 的文章《golang 实现简单网关》才知道还有 httputil.ReverseProxy 这么个东西。
PS:这玩意儿有什么必要放在标准库?
还挺有趣,在作者示例的基础之上完善了一下,实现多服务,多后端节点的一个负载均衡(还可以再补上权重)。
package main
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
)
func main() {
addr := "127.0.0.1:2002"
backends := map[string][]string{
"service1": {"http://127.0.0.1:2003", "http://127.0.0.1:2004"},
}
reversePorxy := NewReverseProxy(backends)
log.Println("Starting httpserver at " + addr)
log.Fatal(http.ListenAndServe(addr, reversePorxy))
}
func reqConvert(req *http.Request, target *url.URL) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
if target.RawQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", "")
}
req.Header.Set("X-Real-Ip", strings.Split(req.RemoteAddr, ":")[0])
}
func NewReverseProxy(backends map[string][]string) *httputil.ReverseProxy {
var targets = make(map[string][]*url.URL)
for srv, nodes := range backends {
for _, nodeUrl := range nodes {
target, _ := url.Parse(nodeUrl)
targets[srv] = append(targets[srv], target)
}
}
director := func(req *http.Request) {
segments := strings.SplitN(req.URL.Path, "/", 3)
if len(segments) != 3 {
return
}
srv := segments[1]
req.URL.Path = segments[2]
if _, ok := targets[srv]; !ok {
log.Printf("unknown path: %s", req.URL.Path)
return
}
rand.Seed(time.Now().UnixNano())
randomIndex := rand.Intn(len(targets[srv]))
target := targets[srv][randomIndex]
reqConvert(req, target)
}
modifyFunc := func(res *http.Response) error {
if res.StatusCode != http.StatusOK {
oldPayLoad, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
newPayLoad := []byte("hello " + string(oldPayLoad))
res.Body = ioutil.NopCloser(bytes.NewBuffer(newPayLoad))
res.ContentLength = int64(len(newPayLoad))
res.Header.Set("Content-Length", fmt.Sprint(len(newPayLoad)))
}
return nil
}
return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyFunc}
}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
package main
import (
"fmt"
"github.com/Shopify/go-lua"
)
func main() {
state := lua.NewState()
defer state.Close()
// 加载 Lua 代码
lua.DoString(state, `
function add(a, b)
return a + b
end
`)
// 调用 Lua 函数
lua.GetGlobal(state, "add")
lua.PushInteger(state, 1)
lua.PushInteger(state, 2)
lua.Call(state, 2, 1)
// 获取 Lua 函数返回值
result := lua.ToInteger(state, -1)
lua.Pop(state, 1)
fmt.Println(result)
}
Ubuntu 更新源中的是 Go 1.18(apt install golang
),现在 Go 1.20 出来了,我想尝尝鲜,就需要考虑多版本共存的方案了。
Python 有 pyenv,Node 有 nvm。
Go 也有一些社区项目,比如 syndbg/goenv 和 moovweb/gvm ,还有 owenthereal/goup 。
其中 gvm 之前有尝试过,参考:gvm: Golang 版本管理
本文是介绍官方的 dl,可以说是非常简单。
go install golang.org/dl/go1.20@latest
~/go/bin/go1.20 download
Downloaded 0.0% ( 16384 / 99869470 bytes) ...
Downloaded 3.5% ( 3522544 / 99869470 bytes) ...
Downloaded 9.8% ( 9748480 / 99869470 bytes) ...
Downloaded 15.7% (15712240 / 99869470 bytes) ...
Downloaded 21.7% (21626880 / 99869470 bytes) ...
Downloaded 27.6% (27541296 / 99869470 bytes) ...
Downloaded 32.9% (32866288 / 99869470 bytes) ...
Downloaded 38.9% (38846464 / 99869470 bytes) ...
Downloaded 44.9% (44793840 / 99869470 bytes) ...
Downloaded 50.8% (50741248 / 99869470 bytes) ...
Downloaded 56.7% (56672240 / 99869470 bytes) ...
Downloaded 62.7% (62586864 / 99869470 bytes) ...
Downloaded 68.1% (67993600 / 99869470 bytes) ...
Downloaded 74.0% (73924304 / 99869470 bytes) ...
Downloaded 79.9% (79839200 / 99869470 bytes) ...
Downloaded 85.9% (85753856 / 99869470 bytes) ...
Downloaded 91.8% (91717424 / 99869470 bytes) ...
Downloaded 97.2% (97025728 / 99869470 bytes) ...
Downloaded 100.0% (99869470 / 99869470 bytes)
Unpacking /home/markjour/sdk/go1.20/go1.20.linux-amd64.tar.gz ...
Success. You may now run 'go1.20'
~/go/bin/go1.20 download
go1.20: already downloaded in /home/markjour/sdk/go1.20
~/sdk/go1.20/bin/go version
go version go1.20 linux/amd64
# sudo ln -sf ~/sdk/go1.20/bin/go /usr/local/bin/go
ln -sf ~/sdk/go1.20/bin/go ~/.local/bin/go1.20
ln -sf go1.20 ~/.local/bin/go
主要是参考一下这个目录,对照着查漏补缺。
package main
var A int = 3
var B int = A + 1
var C int = A
package main
import "fmt"
var D = f()
func f() int {
A = 1
return 1
}
func main() {
fmt.Println(A, B, C)
}
markjour@victus ~/test02
$ go version
go version go1.18.6 windows/amd64
markjour@victus ~/test02
$ go run f1.go f2.go
1 4 3
markjour@victus ~/test02
$ go run f2.go f1.go
1 2 3
f1f2 的情况下:变量初始化的顺序应该为 A B C D,所以输出 1 4 3 是没有问题的。
f2f1 的情况下:变量初始化的顺序应该为 A D B C,所以按理应该输出 1 2 1,实际确实输出 1 2 3。
f1 中 A C 中只要一个的初始化改成这样就可以符合预期:
func initA() int {
return 3
}
func initC() int {
return A
}
这是 Go 的一个 BUG,即将在 Go 1.20 修复。
知道就行了。