#94 Go 1.21 for 语义变更

2023-07-05

仔细观察下面的例子就能知道问题在哪里了:

例子 1

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)
        }
    }
}

例子 2

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()
        }
    }
}

例子 3

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 包中的代码按照旧的定义来。

https://github.com/golang/go/discussions/56010

#93 gotip 命令

2023-05-30

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'!

#92 Go set 类型

2023-05-29

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
}

注意:

  1. 使用 map 做底层存储,因此实现的 set 也是无序的
  2. map 不是线程安全的,如果有并发操作,需要加锁
  3. 如果真的要使用集合类型,应该再扩充一下交集,差集等方法

改进

参考 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
}

优化点

  1. 空结构体不占空间
  2. 泛型让代码复用性更好

#91 Golang 实现简单反向代理

2023-04-27

看到微信公众号 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
}

#90 Golang 程序中内嵌 Lua

2023-04-19
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)
}

#89 Golang 多版本方案

2023-03-09

Ubuntu 更新源中的是 Go 1.18(apt install golang),现在 Go 1.20 出来了,我想尝尝鲜,就需要考虑多版本共存的方案了。

Python 有 pyenv,Node 有 nvm。
Go 也有一些社区项目,比如 syndbg/goenv stars和 moovweb/gvm stars,还有 owenthereal/goup stars

其中 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

#87 Golang 变量初始化顺序问题

2022-12-15

f1.go

package main

var A int = 3
var B int = A + 1
var C int = A

f2.go

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 修复。
知道就行了。

参考资料与拓展阅读