#97 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. 泛型让代码复用性更好

#96 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
}

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

#94 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

#92 Golang 开发环境

2023-01-25
  • Goland
  • VSCode
  • LiteIDE 记得我最早学习 Golang 的时候,有资料建议用 LiteIDE。今后有空了解一下。

VSCode

  • Go: Show All Commands
  • Go: Install/Update Tools
  • Go: Locate Configured Go Tools
  • Go: Current GOPATH
  • Go: Current GOROOT

Go: Install/Update Tools

早一些时候,VSCode 安装 Go 拓展的时候,需要安装这些 Go tools:

gotools

应该是后来有一些工具被逐渐完善的 gopls 取代了,现在工具数量变少了一些:

  • gopls Google 提供的 Languange Server (LSP)
  • gotests 测试工具,提供多种编辑器插件。
  • gomodifytags Modify tags on structs
  • impl Stubs for interfaces
  • goplay The Go playground
  • dlv Go debugger (Delve)
  • staticcheck Linter
# Go 语言的语言服务器协议实现,提供代码补全、错误检查等功能。
go install golang.org/x/tools/gopls@latest
# 自动生成 Go 测试文件的工具。
go install github.com/cweill/gotests/gotests@latest
# 用于修改 Go 结构体标签的工具。
go install github.com/fatih/gomodifytags@latest
# 生成接口实现代码的工具。
go install github.com/joshuarubin/impl@latest
# 运行 Go 代码片段的命令行工具。
go install github.com/haya14busa/goplay/cmd/goplay@latest
# Delve 调试器,用于调试 Go 程序。
go install github.com/go-delve/delve/cmd/dlv@latest
# Go 代码静态分析工具,检查潜在问题和优化点。
go install honnef.co/go/tools/cmd/staticcheck@latest

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

参考资料与拓展阅读

#90 Go DNS 解析

2022-10-30

A 记录

package main

import (
    "fmt"
    "net"
)

func main() {
    // 方法 1
    // func ResolveIPAddr(network, address string) (*IPAddr, error)
    ipAddr, err := net.ResolveIPAddr("ip", "www.google.com")
    if err != nil {
        fmt.Println("解析IP地址失败:", err)

    } else {
        fmt.Println("IP地址是:", ipAddr.IP)
    }

    // 方法 2
    // func LookupMX(name string) ([]*MX, error)
    // func LookupTXT(name string) ([]string, error)
    // func LookupIP(host string) ([]IP, error)
    // func LookupHost(host string) ([]string, error) // only IPv4
    ips, err := net.LookupHost("www.google.com")
    if err != nil {
        fmt.Println("解析主机名失败:", err)
    } else {
        for _, ip := range ips {
            fmt.Println("IP地址是:", ip)
        }
    }
}

MX 记录

package main

import (
    "fmt"
    "net"
)

func main() {
    records, err := net.LookupMX("qq.com")
    if err != nil {
        fmt.Println("解析MX记录失败:", err)
    } else {
        fmt.Printf("%#v", records)
    }
}

指定 DNS 服务器

func queryMX(dns string, domain string) ([]*net.MX, error) {
    resolver := &net.Resolver{
        PreferGo: true,
        Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
            dialer := &net.Dialer{}
            return dialer.DialContext(ctx, "udp", dns)
        },
    }
    return resolver.LookupMX(context.Background(), domain)
}

完整版本

package main

import (
    "context"
    "fmt"
    "net"
    "strings"
    "time"
)

var dnsServers = []string{
    "223.5.5.5:53",
    "114.114.114.114:53",
    "8.8.8.8:53",
}

type IPType int

const (
    NotIP IPType = iota
    IPv4
    IPv6
)

const (
    DNSLookupTimeout = 500 * time.Millisecond // time.Duration
)

func GetIPType(host string) IPType {
    ip := net.ParseIP(host)
    if ip == nil {
        return NotIP
    }
    if ip.To4() != nil {
        return IPv4
    }
    if ip.To16() != nil {
        return IPv6
    }
    return NotIP
}

type Resolver struct {
    DNS      string
    Resolver *net.Resolver
}

func NewResolver(dns string) Resolver {
    resolver := Resolver{}
    resolver.DNS = dns
    resolver.Resolver = &net.Resolver{
        PreferGo: true,
        Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
            dialer := &net.Dialer{}
            return dialer.DialContext(ctx, "udp", dns)
        },
    }
    return resolver
}

func (r Resolver) QueryA(domain string) ([]string, error) {
    return r.Resolver.LookupHost(context.Background(), domain)
}

func (r Resolver) QueryMX(domain string) ([]*net.MX, error) {
    ctx, cancel := context.WithTimeout(context.Background(), DNSLookupTimeout)
    defer cancel()
    return r.Resolver.LookupMX(ctx, domain)
}

func (r Resolver) Query(domain string) ([]*net.MX, error) {
    addrs, err := r.QueryMX(domain)
    if err != nil {
        return nil, err
    }
    var result []*net.MX
    for _, addr := range addrs {
        // hostname := strings.TrimRight(addr.Host, ".")
        ipType := GetIPType(addr.Host)
        fmt.Printf("%s: %#v\n", addr.Host, ipType)
        if ipType == NotIP {
            ips, err := r.QueryA(addr.Host)
            if err != nil {
                continue
            }
            for _, ip := range ips {
                result = append(result, &net.MX{Host: ip, Pref: addr.Pref})
            }
        }
    }
    return result, nil
}

var resolvers map[string]Resolver

func Query(domain string) ([]*net.MX, error) {
    var addrs []*net.MX
    var err error
    for _, dns := range dnsServers {
        var r Resolver
        if r, ok := resolvers[dns]; !ok {
            r = NewResolver(dns)
            resolvers[dns] = r
        }
        addrs, err = r.Query(domain)
        if err == nil {
            break
        }
        fmt.Printf("Error: %s: %s: %#v\n", dns, domain, err)
    }
    return addrs, err
}

func QueryAsync(domain string, ch chan<- []*net.MX, errCh chan<- error) {
    addrs, err := Query(domain)
    if err != nil {
        errCh <- err
        return
    }
    ch <- addrs
}

func init() {
    resolvers = make(map[string]Resolver)
}

func main() {
    {
        domains := []string{"qq.com", "gmail.com", "google.com"}
        for _, domain := range domains {
            fmt.Println(strings.Repeat("=", 100))
            addrs, err := Query(domain)
            if err != nil {
                fmt.Printf("Error: %#v\n", err)
            } else {
                for _, addr := range addrs {
                    fmt.Printf("%#v\n", addr)
                }
            }
        }
    }

    {
        fmt.Println(strings.Repeat("=", 100))
        ch := make(chan []*net.MX)
        errCh := make(chan error)
        go QueryAsync("google.com", ch, errCh)
        select {
        case addrs := <-ch:
            fmt.Println("MX Records:")
            for _, addr := range addrs {
                fmt.Printf("%#v\n", addr)
            }
        case err := <-errCh:
            fmt.Println("Error:", err)
        }
    }
}

#89 gob:Golang 二进制编码格式(标准库)

2022-10-01
package main

import (
    "bytes"
    "encoding/gob"
    "fmt"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    // 编码
    var buf bytes.Buffer
    enc := gob.NewEncoder(&buf)
    err := enc.Encode(Person{"John", 30})
    if err != nil {
        panic(err)
    }

    // 解码
    dec := gob.NewDecoder(&buf)
    var p Person
    err = dec.Decode(&p)
    if err != nil {
        panic(err)
    }

    fmt.Println(p)
}

#88 Golang 源码结构

2022-09-16
# tree -L 1 -d .
.
├── api
├── doc
├── lib
├── misc
├── src
└── test

6 directories
# tree -L 1 -d src/
src/
├── archive
├── bufio
├── builtin
├── bytes
├── cmd
├── compress
├── container
├── context
├── crypto
├── database
├── debug
├── embed
├── encoding
├── errors
├── expvar
├── flag
├── fmt
├── go
├── hash
├── html
├── image
├── index
├── internal
├── io
├── log
├── math
├── mime
├── net
├── os
├── path
├── plugin
├── reflect
├── regexp
├── runtime
├── sort
├── strconv
├── strings
├── sync
├── syscall
├── testdata
├── testing
├── text
├── time
├── unicode
├── unsafe
└── vendor

46 directories
tree -L 2 -d src/net/
src/net/
├── http
│   ├── cgi
│   ├── cookiejar
│   ├── fcgi
│   ├── httptest
│   ├── httptrace
│   ├── httputil
│   ├── internal
│   ├── pprof
│   └── testdata
├── internal
│   └── socktest
├── mail
├── rpc
│   └── jsonrpc
├── smtp
├── testdata
├── textproto
└── url

19 directories