golang 使用总结(1)

1. 写在最前面

笔者最近在赶一个项目,在开发的过程中遇到了几处比较有趣的地方。做个笔记记录,同时方便其人查阅、快速解决类似的问题。

  • 锁拷贝问题(Mutex Copy )
  • 安全的 marshal/ unmarshal 含有锁的结构(Safe marshal/unmarshal structure with lock)
  • 将 Proto 序列化的结构放入到 json 的字段中(Put the structure of the Proto serialization into the json field)

2. 锁拷贝问题

2.1 问题

在 「Google Groups」 里有人提里一个问题:How to copy a struct which contains a mutex?

此处引用 Staven 的回答:

In the words of a great poet: so don’t do that.

Possibly write a copying function that copies only fields that aren’t
a mutex (you probably want to acquire the mutex first).

大意是:用一位伟大诗人的话来说:所以不要那样做。

可能编写一个复制函数,只复制非互斥对象的字段(您可能希望首先获得互斥对象)。

注:而且 go sync package 已经声明 Values containing the types defined in this package should not be copied.

2.2 原因

思考:你确定你真的需要拷贝吗?

答:拷贝操作将拷贝内部状态,这会导致很多麻烦(例如互斥锁)。 创建一个具有相同的、外部可见的状态的新对象要简单得多。

错误拷贝状态的例子

package main

import (
	"fmt"
	"sync"
)

type A struct {
	Lock sync.Mutex
	X    int
}

type B struct {
	A
	Y int
}

func newA(x int) A {
	return A{X: x}
}

func newB(x int, y int) B {
	a := newA(x)
	a.Lock.Lock()
	fmt.Printf("newB a address: %p, %#v\n", &a, &a)
	return B{
		A: a,
		Y: y,
	}
}

func main() {
	b := newB(-1, 3)
	fmt.Printf("main b.A address %p, %#v\n", &b.A, &b.A)
	b.A.Lock.Lock()
}

输出:

mutex $>go run main.go
newB a address: 0x140000140c0, &main.A{Lock:sync.Mutex{state:1, sema:0x0}, X:-1}
main b.A address 0x14000018108, &main.A{Lock:sync.Mutex{state:1, sema:0x0}, X:-1}
fatal error: all goroutines are asleep - deadlock!

注:b.A 的地址跟 a 的地址,但是由于拷贝了 a 的锁,所以在 b.A 再次上锁的时候,导致了崩溃。

3. 安全的 marshal/ unmarshal 含有锁的结构

3.1 问题

问题场景是自定一个结构,其中还有一个 string 字段和一个锁字段。问如何在并发 set/get string 字段的同时,线程安全的对该结构体 marshal/ unmarshal ?

定义的结构如下:

type Status struct {
	AudioErrorMutex *sync.Mutex
	AudioError      string `json:"audioError"`
}

func (s *Status) GetAudioError() string {
	s.AudioErrorMutex.Lock()
	defer s.AudioErrorMutex.Unlock()
	return s.AudioError
}

func (s *Status) SetAudioError(e string) {
	s.AudioErrorMutex.Lock()
	defer s.AudioErrorMutex.Unlock()
	s.AudioError = e
}

3.2 解决

但,为了防止递归调用,一种解决方法是先将 AudioError 单独拆为一个结构体,然后结构体自己实现 MarshalJSON/UnmarshalJSON 的方法。。

package main

import (
	"encoding/json"
	"fmt"
	"sync"
)

type iterm struct {
	AudioError string `json:"audioError"`
}

type Status struct {
	AudioErrorMutex sync.Mutex
	AudioError      iterm `json:"audioError"`
}

func (s *Status) GetAudioError() string {
	s.AudioErrorMutex.Lock()
	defer s.AudioErrorMutex.Unlock()
	return s.AudioError.AudioError
}

func (s *Status) SetAudioError(e string) {
	s.AudioErrorMutex.Lock()
	defer s.AudioErrorMutex.Unlock()
	s.AudioError.AudioError = e
}

func (s *Status) MarshalJSON() ([]byte, error) {
	s.AudioErrorMutex.Lock()
	defer s.AudioErrorMutex.Unlock()
	return json.Marshal(s.AudioError)

}

func (s *Status) UnmarshalJSON(data []byte) error {
	var i iterm
	err := json.Unmarshal(data, &i)
	if err != nil {
		return err
	}
	s.AudioErrorMutex.Lock()
	s.AudioError = i
	s.AudioErrorMutex.Unlock()
	return nil
}

func main() {
	s := Status{}
	s.SetAudioError("hello")
	fmt.Printf("%v\n", &s.AudioError)
	d, err := json.Marshal(&s)
	if err != nil {
		panic(err)
	}

	var p Status
	err = json.Unmarshal(d, &p)
	if err != nil {
		panic(err)
	}
	fmt.Printf("p:%v\n", &p.AudioError)

}

输出:

marshal $> go run main.go
&{hello}
p:&{hello}

4. 将 Proto 序列化的结构放入到 json 的字段中

4.1 问题

此项目中笔者有个需求是,将 proto marshal 的结果存入 json 的 string 中。

注:你要是非得问我为什么有这么奇怪的用法,那只能是因为历史原因了。

示例大概如下:

// json structure 定义
type Info struct {
	Id uint64 `json:"id"`
	Message string `json:"message"`
}

然后将定义好的 proto message marshal 之后的结果存入到 Info.Message 字段。

笔者整理了一个小的复现现场的例子

  • 目录结构:

    .
    ├── example
    │   ├── example.pb.go
    │   └── example.proto
    ├── go.mod
    ├── go.sum
    ├── json  // 二进制文件
    └── main.go
    
  • proto 文件 — example.proto

    syntax = "proto3";
    
    package example;
    
    option go_package = "../example";
    
    message DelayTime {
    	int32 vid = 2; // vid 信息
    	string taskId = 5; // 任务 id
    	string streamId = 6; // 流 id
    	int64 delayTime = 15; // 延迟时间,单位毫秒
    	string eventName = 16; // 上报事件类型
    }
    

    注:生成 example.pb.go 的方法

    ​ 执行 protoc --proto_path=./example --go_out=./example ./example/example.proto

  • 复现问题的 main.go 文件

    package main
    
    import (
    	"crypto/md5"
    	"encoding/hex"
    	"encoding/json"
    	"fmt"
    
    	proto "example/jsonAndPb/example"
    
    	protoc "github.com/golang/protobuf/proto"
    )
    
    func md5V(str string) string {
    	h := md5.New()
    	h.Write([]byte(str))
    	return hex.EncodeToString(h.Sum(nil))
    }
    
    type Hello1 struct {
    	Content string `json:"content"`
    }
    
    func Marshal1(b []byte) ([]byte, error) {
    	fmt.Println("marshal", md5V(string(b)))
    	h1 := Hello1{
    		Content: string(b),
    	}
    	b1, err := json.Marshal(h1)
    	if err != nil {
    		return nil, err
    	}
    	return b1, nil
    }
    
    func Unmarshal1(b []byte) error {
    	var h1 Hello1
    	err := json.Unmarshal(b, &h1)
    	if err != nil {
    		return err
    	}
    	fmt.Println("unmarsal1", md5V(h1.Content))
    	return nil
    }
    
    func main() {
    	r := proto.DelayTime{
    		Vid:       10086,
    		TaskId:    "task_id",
    		EventName: "test",
    		DelayTime: 1000,
    	}
    	b, err := protoc.Marshal(&r)
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	data, err := Marshal1(b)
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	err = Unmarshal1(data)
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    }
    

    输出:

    jsonAndPb $> go build -o json
    jsonAndPb $> ./json
    marshal1 9935f3909ef5048a7028e60078a87f97
    unmarsal1 746d3b114729e5f709c4d87aec86d307
    

    结论:

    • proto marshal 之后的二进制在 json marshal 之前是 9935f3909ef5048a7028e60078a87f97
    • json unmarshal 之后是 746d3b114729e5f709c4d87aec86d307
    • 这会导致在 proto unmarshal 的时候报 proto: cannot parse invalid wire-format data

4.2 解决

核心思想是将 proto marshal 的二进制 -> base64 编码 -> 放入 json 中。

注:此处用一个比较取巧的方法,直接修改 Hello1 的定义将 Content 的类型改为 []byte。

Array and slice values encode as JSON arrays, except that []byte encodes as a base64-encoded string, and a nil slice encodes as the null JSON value.(数组和切片值编码为 JSON 数组,除了 []byte 编码为 base64 编码字符串,nil 切片编码为空 JSON 值。)

修改后的 main.go 文件

package main

import (
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"

	proto "example/jsonAndPb/example"

	protoc "github.com/golang/protobuf/proto"
)

func md5V(str string) string {
	h := md5.New()
	h.Write([]byte(str))
	return hex.EncodeToString(h.Sum(nil))
}

type Hello2 struct {
	Content []byte `json:"content"`
}

func Marshal2(b []byte) ([]byte, error) {
	fmt.Println("marshal2", md5V(string(b)))
	h2 := Hello2{
		Content: b,
	}
	b1, err := json.Marshal(h2)
	if err != nil {
		return nil, err
	}
	return b1, nil
}

func Unmarshal2(b []byte) error {
	var h2 Hello2
	err := json.Unmarshal(b, &h2)
	if err != nil {
		return err
	}
	fmt.Println("unmarsal2", md5V(string(h2.Content)))
	return nil
}

func main() {
	r := proto.DelayTime{
		Vid:       10086,
		TaskId:    "task_id",
		EventName: "test",
		DelayTime: 1000,
	}
	b, err := protoc.Marshal(&r)
	if err != nil {
		fmt.Println(err)
		return
	}
	data, err := Marshal2(b)
	if err != nil {
		fmt.Println(err)
		return
	}
	err = Unmarshal2(data)
	if err != nil {
		fmt.Println(err)
		return
	}
}

输出结果:

jsonAndPb $> go build -o json
jsonAndPb $> ./json
marshal2 9935f3909ef5048a7028e60078a87f97
unmarsal2 9935f3909ef5048a7028e60078a87f97

5. 碎碎念

又进入了疫情爆发的阶段 ,自己和家人在的地方疫情都很严重。

前几天吹蜡烛的时候,悄悄许了一个愿望:「希望疫情能早点消失。」(ps 嗯,是被身边的人感动的一个月。

  • 一切都是成长,包括热泪盈眶。
  • 我们终其一生都在成长,愿都能眼里有光,活成自己喜欢的模样。

6. 参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值