本文主要介绍Go实现浅拷贝和深拷贝,以及工具库mohae/deepcopy。

定义

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源

深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。比较典型的就是Value(值)对象,如预定义类型Int32,Double,以及结构(struct),枚举(Enum)等

浅拷贝

go使用copy内置函数将源片中的元素复制到目标切片,如果拷贝的 结构中不包含指针是没问题,否则则数据源和拷贝之间对应指针会共同指向同一块内存。

func shallowCopyReference() {
	brazil := []*Player{{"Neymar"}, {"Messi"}}
	argentina := make([]*Player, len(brazil))
	copy(argentina, brazil)
	fmt.Printf("%p, %p\n", brazil, brazil[0])
	fmt.Printf("%p, %p\n", argentina, argentina[0])
	brazil[0].Name = "Anthony"
	fmt.Printf("%p, %v, %p\n", brazil, brazil[0], brazil[0])
	fmt.Printf("%p, %v, %p\n", argentina, argentina[0], brazil[0])
}
//output
0xc0000412e0, 0xc0000412f0
0xc000041310, 0xc0000412f0
0xc0000412e0, &{Anthony}, 0xc0000412f0
0xc000041310, &{Anthony}, 0xc0000412f0 

如上,修改了src(拷贝原始结构)的指针对象,dst(拷贝目标结构)的值也被改动了,显然是不行的。

深拷贝

方法一:gob、json 序列化成字节序列再反序列化生成克隆对象

  • gob序列化
func deepCopyByGobExample() {
	players := append([]*Player{}, &Player{Name: "Neymar"}, &Player{Name: "Messi"})
	brazil := &Team{Players: players}
	argentina := &Team{}
	err := deepCopyByGob(brazil, argentina)
	if err != nil {
		fmt.Errorf("deepcopy error:", err)
	}
	fmt.Printf("%v,%v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v,%v, %p\n", "argentina", argentina.Players[0], argentina.Players[0])
	brazil.Players[0].Name = "Anthony"
	fmt.Printf("%v, %v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v, %v, %p\n", "argentina", argentina.Players[0], brazil.Players[0])
}

func deepCopyByGob(dst, src *Team) error {
	var buf bytes.Buffer
	if err := gob.NewEncoder(&buf).Encode(src); err != nil {
		return err
	}
	return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
}
//output
brazil,&{Neymar}, 0xc000184ff0
argentina,&{Neymar}, 0xc000185330
brazil, &{Anthony}, 0xc000184ff0
argentina, &{Neymar}, 0xc000184ff0 
  • json序列化
func deepcopyByJsonExample() {
	players := append([]*Player{}, &Player{Name: "Neymar"}, &Player{Name: "Messi"})
	brazil := &Team{Players: players}
	argentina := &Team{}
	err := deepCopyByJson(brazil, argentina)
	if err != nil {
		fmt.Errorf("deepcopy error:", err)
	}
	fmt.Printf("%v,%v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v,%v, %p\n", "argentina", argentina.Players[0], argentina.Players[0])
	brazil.Players[0].Name = "Anthony"
	fmt.Printf("%v, %v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v, %v, %p\n", "argentina", argentina.Players[0], brazil.Players[0])
}

func deepCopyByJson(src, dst *Team) error {
	if tmp, err := json.Marshal(src); err != nil {
		return err
	} else {
		err = json.Unmarshal(tmp, dst)
		return err
	}
}
//output
brazil,&{Neymar}, 0xc0000412e0
argentina,&{Neymar}, 0xc000041490
brazil, &{Anthony}, 0xc0000412e0
argentina, &{Neymar}, 0xc0000412e0

可以看到deepcopy出来的指针地址不一样,那么都dst的修改就不会影响到src了。

方法二:使用第三方工具库

DeepCopy对事物进行深度复制:未导出的字段值不会被复制。 feature:

  • copy slice
  • copy map
func deepCopyExample() {
	brazil := &Team{Players: append([]*Player{}, &Player{Name: "Neymar"}, &Player{Name: "Messi"})}
	argentinaIfc := deepcopy.Copy(brazil)
	argentina := argentinaIfc.(*Team)
	fmt.Printf("%v,%v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v,%v, %p\n", "argentina", argentina.Players[0], argentina.Players[0])
	brazil.Players[0].Name = "Anthony"
	fmt.Printf("%v, %v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v, %v, %p\n", "argentina", argentina.Players[0], brazil.Players[0])
}
//output
brazil,&{Neymar}, 0xc0000412e0
argentina,&{Neymar}, 0xc000041330
brazil, &{Anthony}, 0xc0000412e0
argentina, &{Neymar}, 0xc0000412e0

小结

浅拷贝是值的拷贝,如果属性是引用,拷贝的就是内存地址,需要使用深拷贝。深拷贝的实现原理本质上是通过反射实现。通过将源对象转换成接口,再对接口通过反射判断其类型。所以可以通过序列化成gob、json等来实现。工具库mohae/deepcopy可以对切片、map、结构体、接口进行深拷贝,是个不错的选择。

参考

go语言提供非常方便的创建轻量级的协程goroutine来并发处理任务,但是 协程过多影响程序性能,所以,这时goroutine池就登场了。本文简要介绍ants goroutine 池的基本的使用方法。

简介

ants是一个高性能的 goroutine 池,实现了对大规模 goroutine 的调度管理、goroutine 复用,允许使用者在开发并发程序的时候限制 goroutine 数量,复用资源,达到更高效执行任务的效果。 ants就是一个很多大厂广泛使用的goroute池。

官网地址

https://github.com/panjf2000/ants

功能

  • 自动调度海量的 goroutines,复用 goroutines
  • 定期清理过期的 goroutines,进一步节省资源
  • 提供了大量有用的接口:任务提交、获取运行中的 goroutine 数量、动态调整 Pool 大小、释放 Pool、重启 Pool
  • 优雅处理 panic,防止程序崩溃
  • 资源复用,极大节省内存使用量;在大规模批量并发任务场景下比原生 goroutine 并发具有更高的性能
  • 非阻塞机制

quick start

使用 ants v1 版本:

go get -u github.com/panjf2000/ants

使用 ants v2 版本 (开启 GO111MODULE=on):

go get -u github.com/panjf2000/ants/v2

接下来看一下官方demo:实现一个计算大量整数和的程序

NewPool

NewPool生成ants池实例。

package main

import (
	"fmt"
	"sync"
	"time"

	"github.com/panjf2000/ants/v2"
)

var sum int32

func demoFunc() {
	time.Sleep(10 * time.Millisecond)
	fmt.Println("Hello World!")
}

func main() {
	defer ants.Release()

	runTimes := 1000

	//使用公共池。
	var wg sync.WaitGroup
	syncCalculateSum := func() {
		demoFunc()
		wg.Done()
	}
	for i := 0; i < runTimes; i++ {
		wg.Add(1)
		_ = ants.Submit(syncCalculateSum)
	}
	wg.Wait()
	fmt.Printf("running goroutines: %d\n", ants.Running())
    fmt.Printf("finish all tasks.\n")
 }

其中:

生成一个具有特定函数的ants池实例

  • NewPool(size int, options …Option) (*PoolWithFunc, error)

args.size 即池容量,即池中最多有 10 个 goroutine。 arg.Option 定制化 goroutine pool.

  • p.Submit向此池提交任务。
  • ants.Release关闭此池并释放工作队列。
  • defaultAntsPool 导入ants时初始化实例池。

NewPoolWithFunc

package main

import (
	"fmt"
	"github.com/panjf2000/ants/v2"
	"sync"
	"sync/atomic"
)

var sum int32

func myFunc(i interface{}) {
	n := i.(int32)
	atomic.AddInt32(&sum, n)
	fmt.Printf("run with %d\n", n)
}

func main() {
	defer ants.Release()
	runTimes := 1000
	var wg sync.WaitGroup
	//使用带有函数的池
//设置goroutine池的容量为10,过期时间为1秒。
	p, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
		myFunc(i)
		wg.Done()
	})
	defer p.Release()
	//逐个提交任务。
	for i := 0; i < runTimes; i++ {
		wg.Add(1)
		_ = p.Invoke(int32(i))
	}
	wg.Wait()
	fmt.Printf("running goroutines: %d\n", p.Running())
	fmt.Printf("finish all tasks, result is %d\n", sum)
}

OutPut:

run with 1
run with 5
run with 11
run with 12
run with 13
run with 14
run with 7
...
run with 789
run with 809
running goroutines: 10
finish all tasks, result is 499500

其中:

生成一个具有特定函数的ants池实例

  • NewPoolWithFunc(size int, pf func(interface{}), options …Option) (*PoolWithFunc, error)

args.pf 即为执行任务的函数

优雅处理 panic

测试一些当任务触发panic的情况

func myFunc(i interface{}) {
	n := i.(int32)
	atomic.AddInt32(&sum, n)
	if n%2 == 0 {
		panic(any(fmt.Sprintf("panic from task:%d", n)))
	}
	fmt.Printf("run with %d\n", n)
}

output:

run with 3
run with 13
...
run with 999
2022/10/21 21:41:05 worker with func exits from a panic: panic from task:6
2022/10/21 21:41:05 worker with func exits from a panic: panic from task:0
2022/10/21 21:41:07 worker with func exits from panic: goroutine 14 [running]:

可以看到,main routine 没有因此受影响。

Options

// Options包含实例化ants池时将应用的所有选项。
type Options struct {
	// ExpiryDuration is a period for the scavenger goroutine to clean up those expired workers,
	// the scavenger scans all workers every `ExpiryDuration` and clean up those workers that haven't been
	// used for more than `ExpiryDuration`.
	ExpiryDuration time.Duration

	// PreAlloc indicates whether to make memory pre-allocation when initializing Pool.
	PreAlloc bool

	// Max number of goroutine blocking on pool.Submit.
	// 0 (default value) means no such limit.
	MaxBlockingTasks int

	// When Nonblocking is true, Pool.Submit will never be blocked.
	// ErrPoolOverload will be returned when Pool.Submit cannot be done at once.
	// When Nonblocking is true, MaxBlockingTasks is inoperative.
	Nonblocking bool

	// PanicHandler is used to handle panics from each worker goroutine.
	// if nil, panics will be thrown out again from worker goroutines.
	PanicHandler func(interface{})

	// Logger is the customized logger for logging info, if it is not set,
	// default standard logger from log package is used.
	Logger Logger
}

比如 PanicHandler 遇到 panic会调用这里设置的处理函数,以上例中,我们遇到偶数会触发panic,修改NewPool函数

p, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
		myFunc(i)
		wg.Done()
	}, ants.WithPanicHandler(func(i interface{}) {
		fmt.Printf("panic recover %v", i)
	}))

out:

panic recover panic from i:992run with 880
panic recover panic from i:880run with 972
panic recover panic from i:972run with 994
run with 991

还有更多关于 Benchmarks 性能报告,可参考https://github.com/panjf2000/ants

参考:

前后端数据常用传输格式有:json、xml 和 proto等,不管是mvc还是ddd,都会在表现层的对不同的格式进行转化下层依赖所需的类、Object、 aggregate等。

Kratos 表现层

在实际开发场景中,前后端传输采用json。 但kratos使用proto定义Api, 那么我们以一个Http请求为例,来看看kratos的表现层是如何处理的。

以下是一个Kratos-Example,由编写好的proto文件,proto-go 等自动生成表现层的代码。 大概步骤:

  1. 编写proto,包括Service,Req,Reply
  2. 生成表现层代码
  3. 实现下层逻辑等

Example完整代码如下

func _Greeter_SayHello0_HTTP_Handler(srv GreeterHTTPServer) func(ctx http.Context) error {
	return func(ctx http.Context) error {
		var in HelloRequest
		if err := ctx.BindQuery(&in); err != nil {
			return err
		}
		if err := ctx.BindVars(&in); err != nil {
			return err
		}
		http.SetOperation(ctx, "/helloworld.v1.Greeter/SayHello")
		h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
			return srv.SayHello(ctx, req.(*HelloRequest))
		})
		out, err := h(ctx, &in)
		if err != nil {
			return err
		}
		reply := out.(*HelloReply)
		return ctx.Result(200, reply)
	}
}

可以看到:

  1. 这里对具体的格式无感,输入In还是输出Out都是一个proto生成的类。
  2. 具体的实现交给了ctx 上下文去实现 查看 ctx.Result的源码:
func (c *wrapper) Result(code int, v interface{}) error {
	c.w.WriteHeader(code)
	return c.router.srv.enc(&c.w, c.req, v)
}

这里的enc,没有特殊设置,使用默认的DefaultResponseEncoder,部分源码如下

func DefaultResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
	...
	codec, _ := CodecForRequest(r, "Accept")
	data, err := codec.Marshal(v)
	if err != nil {
		return err
	}
	w.Header().Set("Content-Type", httputil.ContentType(codec.Name()))
	_, err = w.Write(data)
    ...
}

通过codc接口隔离具体实现,调用接口的 codec.Marshal序列化。

codec包

Codec定义Transport用于编码和解码消息的接口。它的实现包括 json,xml,proto,yaml等

type Codec interface {
	// Marshal returns the wire format of v.
	Marshal(v interface{}) ([]byte, error)
	// Unmarshal parses the wire format into v.
	Unmarshal(data []byte, v interface{}) error
	// Name returns the name of the Codec implementation. The returned string
	// will be used as part of content type in transmission.  The result must be
	// static; the result cannot change between calls.
	Name() string
}

这里我们聚焦正在使用的Json

type codec struct{}
func (codec) Marshal(v interface{}) ([]byte, error) {
	switch m := v.(type) {
	case json.Marshaler:
		return m.MarshalJSON()
	case proto.Message:
		return MarshalOptions.Marshal(m)
	default:
		return json.Marshal(m)
	}
}

这里Marshal()使用第三方库““google.golang.org/protobuf/encoding/protojson”

实现proto转json。

protojson第三方包

protojson是Google提供的proto和json的转化

package api_test

import (
	"google.golang.org/protobuf/encoding/protojson"
	"testing"

	v1 "helloworld/api/helloworld/v1"
)

func TestToJson(t *testing.T) {
	reply := &v1.HelloReply{
		Message: "Jobs",
	}
	replyJson, err := MarshalOptions.Marshal(reply.ProtoReflect().Interface())
	if err != nil {
		t.Errorf("Marshal Error: %v", err)
	}
	t.Logf("replyJson:  %v", string(replyJson))
}

更多的接口请参考“https://google.golang.org/protobuf/encoding/protojson”

小结

本文主要以kratos的表现层的组织方式,来介绍常用的框架表现层处理方式。 其中:

  • kratos通过proto来生成表现层代码的类,包括http和grapc,内部对具体实现无感。
  • kratos通过解码器codec实现不同传输格式的解耦。
  • 第三方包protojson实现proto和json转换。

框架的主要功能之一就是标准化处理一下公共的问题场景,从源码中可以学习:通过接口隔离具体的实现,而使框架与具体实现解耦,且具备更好的拓展性。

参考

本文主要介绍go如何读取和写入csv文件,以及使用第三方库gocsv转换为Struct。

读取csv

go 标准库 “encoding/csv” 用于读写csv文件。

Reader从csv编码的文件中读取记录

  • type Reader
    • func NewReader(r io.Reader) *Reader
    • func (r *Reader) Read() (record []string, err error) 。
    • func (r *Reader) ReadAll() (records [][]string, err error)

创建一个csv文件servers.csv,具体如下

world-svc,1/1,Running,0,44m
battle-svc,1/1,Running,0,7d

ReadAll从r中读取所有剩余的记录,每个记录都是字段的切片,成功的调用返回值err为nil而不是EOF。因为ReadAll方法定义为读取直到文件结尾,因此它不会将文件结尾视为应该报告的错误。

package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"os"
)
func main() {
	readeCsvAll() 
}
func readerCsvReadAll() {
	file, err := os.Open("servers.csv")
	defer file.Close()
	if err != nil {
		fmt.Println(err)
	}
	reader := csv.NewReader(file)
	servers, _ := reader.ReadAll()
	fmt.Println("ReadAll:", servers)
}

OutPut:

ReadAll:  [world-svc 1/1 Running 0 44m] [battle-svc 1/1 Running 0 7d]]

Read从r读取一条记录,返回值record是字符串的切片,每个字符串代表一个字段。

reader := csv.NewReader(file)
for {
    servers, err := reader.Read()
    if err == io.EOF {
        break
    } else if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(servers)
}

输出和ReadAll是一样的。

csv读写逗号分隔值(csv)的文件,亦或可以定义数据的分隔符

reader.Comma = '|' 

亦或可以定义忽略的的行

reader.Comment = '#'

写入csv

Writer类型的值将记录写入一个csv编码的文件

  • type Writer
    • func NewWriter(w io.Writer) *Writer
    • func (w *Writer) Write(record []string) (err error)
    • func (w *Writer) WriteAll(records [][]string) (err error)
    • func (w *Writer) Flush()
    • func (w *Writer) Error() error

向w中写入一条记录,会自行添加必需的引号。记录是字符串切片,每个字符串代表一个字段。

func writerOne() {
	servers := []Server{
		{"world-svc", "1/1", "Running", 0, "44m"},
		{"battle-svc", "1/1", "Running", 0, "7d"},
	}
	file, err := os.Create("serversA.csv")
	defer file.Close()
	if err != nil {
		log.Fatalln("failed to open file", err)
	}
	w := csv.NewWriter(file)
   //将缓存中的数据写入底层的io.Writer。
	defer w.Flush()
	// 使用 Write
	for _, server := range servers {
		row := []string{server.Name, server.Ready, server.State, strconv.Itoa(server.Restarts), server.Age}
		if err := w.Write(row); err != nil {
			log.Fatalln("error writing server to file", err)
		}
	}

执行,在当前目录生成 serversA.csv,内容如下:

world-svc,1/1,Running,0,44m
battle-svc,1/1,Running,0,7d

WriteAll方法使用Write方法向w写入多条记录,并在最后调用Flush方法清空缓存

	var data [][]string
	for _, server := range servers {
		row := []string{server.Name, server.Ready, server.State, strconv.Itoa(server.Restarts), server.Age}
		data = append(data, row)
	}
	w.WriteAll(data)

To Struct

GoCSV包旨在提供 CSV 和 Go (golang) 值之间的快速和惯用的映射。 已有servers.csv 内容如下

Name,Ready,Status,Restart,Age
world-svc,1/1,Running,0,44m
battle-svc,1/1,Running,0,7d

UnmarshalFile(in *os.File, out interface{}) error UnmarshalFile从接口中的文件解析CSV。

func ToStruct() {
	clientsFile, err := os.OpenFile("servers.csv", os.O_RDWR|os.O_CREATE, os.ModePerm)
	if err != nil {
		panic(err)
	}
	defer clientsFile.Close()

	var servers []*Server
    //转换成 Server Object
	if err := gocsv.UnmarshalFile(clientsFile, &servers); err != nil {  
		panic(err)
	}
	for _, server := range servers {
		fmt.Println("Server", server.Name)
	}
}

OuPut:

Server world-svc
Server battle-svc

更多api使用方式可参考:https ://github.com/gocarina/gocsv

参考

Go Corntab

本文简要介绍golang 使用crontab实现定时任务。

linux crontab

Linux crontab 是用来定期执行程序的命令。当安装完成操作系统之后,默认便会启动此任务调度命令

命令语法:

crontab [ -u user ] file

crontab [ -u user ] { -l | -r | -e }

*    *    *    *    *
-    -    -    -    -
|    |    |    |    |
|    |    |    |    +----- 星期中星期几 (0 - 6) (星期天 为0)
|    |    |    +---------- 月份 (1 - 12) 
|    |    +--------------- 一个月中的第几天 (1 - 31)
|    +-------------------- 小时 (0 - 23)
+------------------------- 分钟 (0 - 59)

go cron

第三方包“github.com/robfig/cron”来创建 crontab,以实现定时任务

package main

import (
	"fmt"
	"github.com/robfig/cron"
)

func main() {
	var (
		cronS = cron.New()
		spec  = "*/2 * * * * "
		count = 0
	)

	entityID, err := cronS.AddFunc(spec, func() {
		count++
		fmt.Println("count: ", count,"now:", time.Now().Unix())
	})
	if err != nil {
		fmt.Errorf("error : %v", err)
		return
	}
	cronS.Start()

	fmt.Println(entityID)
	defer cronS.Stop()
	select {}
}

go run main.go

输出:

count:  1 now: 1658053860
count:  2 now: 1658053920
count:  3 now: 1658053980
count:  4 now: 1658054040
count:  5 now: 1658054100

可以看到每隔1分钟,执行一次Func,count++

默认情况下标准 cron 规范解析(第一个字段是“分钟”) 可以轻松选择进入秒字段。

cronS = cron.New(cron.WithSeconds())
//注意这里多了一个参数
spec  = "*/2 * * * * * "

执行输出

count:  1 now: 1658053640
count:  2 now: 1658053642
count:  3 now: 1658053644
count:  4 now: 1658053646
count:  5 now: 1658053648 

可以看到每隔两秒执行一次

本文主要记录 Android app 第三方(google和meta/facebook)登入的后端(go)验证。

android sdk 接入流程

可参考 https://juejin.cn/post/7094889100389384228

google

官网链接: (https://developers.google.com/identity/sign-in/web/backend-auth) 官网推荐两中验证方式 :

  1. 使用谷歌API客户端库,包括Java、Node.js、PHP、Python,是在生产环境中验证谷歌ID令牌的推荐方法。go客户端库:https://github.com/googleapis/google-api-go-client)
  2. 调用谷歌API(https://oauth2.googleapis.com/tokeninfo?id_token=XYZ123)

这里采用第一种,参考代码:

package main

import (
	"context"
	"fmt" 
	"google.golang.org/api/oauth2/v2"
	"google.golang.org/api/option"
	"net/http"
)

func main() {
   // 从客户端获取的谷歌token
	googleToken := `user token from client`
	oatuService, err := oauth2.NewService(context.Background(), option.WithHTTPClient(http.DefaultClient))
	if err != nil { 
		fmt.Println(err)
	}
	tokenInfoCall := oatuService.Tokeninfo()
	tokenInfoCall.IdToken(googleToken)
	tokenInfo, err := tokenInfoCall.Do() 
	if err != nil { 
		fmt.Println(err)
	}
	fmt.Println(tokenInfo)
	fmt.Printf("%v", tokenInfo)
    //可以拿客户端发送的id,和 tokenInfo.Id 做校验
}

meta(facebook) 登入

meta 的验证和 google 类型,调用验证api

官网链接:

  1. Android版本Facebook快速入门:https://developers.facebook.com/docs/facebook-login/android
  2. 后端验证 : (https://developers.facebook.com/docs/facebook-login/guides/%20access-tokens/get-session-info)

请求格式:

GET /debug_token?
  input_token={session-info-token}&
  access_token={your-access-token}

go-api : https://developers.facebook.com/docs/facebook-login/guides/%20access-tokens/get-session-info

package main

import (
	"fmt"
    "net/http"
	fb "github.com/huandu/facebook/v2"
)

func main() {
   //客户端传递过来的 token
	inputToken := "token from client"
	globalApp := fb.New("developer-app-client-id", "develop-app-client-secret")
    //生成 access_token
	token := globalApp.AppAccessToken()
	session := globalApp.Session(token)
	resp, err := session.Get("debug_token", fb.Params{
		"input_token": inputToken,
	})
	if err != nil {
		return
	}
}

参考

本文主要介绍 jinzhu/copier 的使用和常用场景

简介

在go后端项目开发中,内部rpc服务返回的字段跟api服务相差无几,一个个赋值比较费事儿。那么就需要Object、List、HashMap 等进行值拷贝。jinzhu/copier即提供了这些场景的支持。

copier 特性

  • 从方法复制到具有相同名称的字段
  • 从字段复制到具有相同名称的方法
  • 从一个切片复制到另一个切片
  • 从结构体复制到切片
  • 从map复制到map
  • 强制复制带有标记的字段
  • 忽略带有标记的字段
  • 深拷贝

Usage

Copy from struct

package main

import (
	"github.com/jinzhu/copier"
	"testing"
)

type User struct {
	Name         string
	Role         string
	Age          int32
	EmployeeCode int64 `copier:"EmployeeNum"` // specify field name

	// Explicitly ignored in the destination struct.
	Salary int
}

//目标结构体中的标签提供了copy指令。复制忽略
//或强制复制,如果字段没有被复制则惊慌或返回错误。
type Employee struct {
	//告诉copier。如果没有复制此字段,则复制到panic。
	Name string `copier:"must"`

	//告诉copier。 如果没有复制此字段,则返回错误。
	Age int32 `copier:"must,nopanic"`

	// 告诉copier。 显式忽略复制此字段。
	Salary int `copier:"-"`

	DoubleAge  int32
	EmployeeId int64 `copier:"EmployeeNum"` // 指定字段名
	SuperRole  string
}

func TestCopyStruct(t *testing.T) {
	var (
		user     = User{Name: "Jinzhu", Age: 18, Role: "Admin", Salary: 200000}
		employee = Employee{Salary: 150000}
	)
	copier.Copy(&employee, &user)
	t.Logf("%#v \n", employee)
}

output:

    copier_test.go:47: main.Employee{Name:"Jinzhu", Age:18, Salary:150000, DoubleAge:36, EmployeeId:0, SuperRole:"Super Admin"} 

Copy from slice to slice

func TestCopySlice(t *testing.T) {
	var (
		users     = []User{{Name: "Jinzhu", Age: 18, Role: "Admin", Salary: 100000}, {Name: "jinzhu 2", Age: 30, Role: "Dev", Salary: 60000}}
		employees = []Employee{}
	)
	employees = []Employee{}
	copier.Copy(&employees, &users)

	t.Logf("%#v \n", employees)
}`

output :

    copier_test.go:57: []main.Employee{main.Employee{Name:"Jinzhu", Age:18, Salary:0, DoubleAge:36, EmployeeId:0, SuperRole:"Super Admin"}, main.Employee{Name:"jinzhu 2", Age:30, Salary:0, DoubleAge:60, EmployeeId:0, SuperRole:"Super Dev"}} 

Copy from Map to Map

func TestCopyMap(t *testing.T) {
	// Copy map to map
	map1 := map[int]int{3: 6, 4: 8}
	map2 := map[int32]int8{}
	copier.Copy(&map2, map1)

	t.Logf("%#v \n", map2)
}

output :

    copier_test.go:66: map[int32]int8{3:6, 4:8} 

场景 1(rpc&api)

实际开发中,免不了服务间通讯,比较前文所说的场景,一个内部的rpc服务返回的参数和api服务差不多,那么就可以使用copier。

//伪代码如下
func ApiLogin(ctx context.Context,request *api.LoginRequest)(reply *api.LogingReply,err error)  {
	grpcClient := v1.NewGameGrpcClient(ctx)
	reply, err := client.Login(ctx, &grpc.api.LoginRequest{ 
					})
	user := api.LogingReply.User{}
 	copier.Copy(&user, reply.User())
return &api.LoginReply{
	User:user,
},err

场景 2 (model-object/aggregate)

实际开发中,不管是mvc\ddd 都会有从model到object/aggreate的repository,那么就可以使用copier。

func (r *UserRepo) Get(ctx context.Context, uid int64) (u User,err error) {
	model, err := db.User.Get(ctx, uid)
	if err != nil {
		return
	}
	obj:= User{}
	copy(&obj,model)
	return obj,nil
}

小结

copier提供不同类型之间相同的字段名,使用tag或者方法支持不同的字段名的赋值。减少一些重复的工作量,小巧实用。

参考

本文主要介绍 在go开发中 errors 的处理和第三方库 github.com/pkg/errors 的使用。

error interface

官方定义:

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
	Error() string
}

常用的声明方式

//方式一
err1 := fmt.Errorf("io.EOF")
//方式二
err2 := errors.New("io.EOF")
//方式三: 实现interface
type err3 struct{
}
func (e err3) Error() string {
	return "err3"
}

go的错误常见是把错误层层传递,这样可能带来一些不友好的地方:

  • 错误判断,经过层次包裹的错误,使用层不好判断真实错误
  • 定位错误,error库过于简单,没有提供粗错误堆栈,不好定位问题

错误判断

常见错误的判断方式是:


type base struct{}

func (e base) Error() string {
	return "base error"
}

func wrapBase() error {
	return fmt.Errorf("wrapBase: %w", base{})
}

func TestErrors(t *testing.T) {
	t.Run("==", func(t *testing.T) {
		foo := &base{}
		bar := &base{}
		t.Log(foo)
		t.Log(bar)
		t.Log(foo == bar)
		assert.True(t, foo == bar)
	})
	t.Run("断言", func(t *testing.T) {
		var err error
		_, ok := err.(*base)
		assert.False(t, ok)
	})
	t.Run("Is", func(t *testing.T) {
		foo := base{}
		wrapFoo := wrapBase()
		assert.False(t, foo == wrapFoo)
		assert.True(t, errors.Is(wrapFoo, foo))
		assert.False(t, errors.Is(wrapFoo, &base{}))
	})
	t.Run("As", func(t *testing.T) {
		foo := base{}
		wrapFoo := wrapBase()
		assert.False(t, foo == wrapFoo)
		assert.True(t, errors.As(wrapFoo, &base{}))
	})
}

OutPut:

--- PASS: TestErrors (0.00s)
--- PASS: TestErrors/== (0.00s)
--- PASS: TestErrors/断言 (0.00s)
--- PASS: TestErrors/Is (0.00s)
 --- PASS: TestErrors/As (0.00s)
  • 当没有嵌套错误,可以使用 ==来判断;
  • 当有多层嵌套,可以使用 Is() :
    • //一个错误被认为匹配一个目标,如果它等于那个目标或如果它实现了一个方法Is(error) bool,使Is(target)返回true。
    • // As在err’s chain中找到第一个与target匹配的错误,如果找到,则设置指向错误值并返回true。否则,返回false

定位错误

常用的第三方库

github.com/pkg/errors/errors.go 提供了以下功能:

  • WithStack 包装堆栈
  • WithMessagef 包装异常
func TestWithStackCompare(t *testing.T) {
	t.Run("fmt.Errorf,无堆栈,不好定位", func(t *testing.T) {
		err1 := fmt.Errorf("io.EOF")
		fmt.Printf("err1: %+v", err1)
	})
	t.Run("errors ,无堆栈,不好定位", func(t *testing.T) {
		err2 := errors.New("io.EOF")
		fmt.Printf("err2: %+v", err2)
	})
	t.Run("pkgerrors,有堆栈,方便定位", func(t *testing.T) {
		err3 := pkgerrors.WithStack(io.EOF)
		fmt.Printf("err3: %+v", err3)
	})

}

OutPut:

=== RUN   TestWithStackCompare
=== RUN   TestWithStackCompare/fmt.Errorf,无堆栈,不好定位
err1: io.EOF=== RUN   TestWithStackCompare/errors_,无堆栈,不好定位
err2: io.EOF=== RUN   TestWithStackCompare/pkgerrors,有堆栈,便以定位
err3: EOF
tkingo.vip/egs/goerrors-demo.TestWithStackCompare.func3
	$ workspace/goerrors-demo/errors_test.go:113
testing.tRunner
	$ workspace/Go/src/testing/testing.go:1439
runtime.goexit

func TestWithMessagef(t *testing.T) {
	tests := []struct {
		err     error
		message string
		want    string
	}{
		{io.EOF, "read error", "read error: EOF"},
		{pkgerrors.WithMessagef(io.EOF, "read error without format specifier"), "client error", "client error: read error without format specifier: EOF"},
		{pkgerrors.WithMessagef(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"},
	}

	for _, tt := range tests {
		got := pkgerrors.WithMessagef(tt.err, tt.message).Error()
		if got != tt.want {
			t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want)
		}
	}
}

总结

pkg errors 应该能够支持错误堆栈、不同的打印格式很好的补充了go errors 的一些短板

参考:

  • github.com/pkg/errors

本文主要介绍一个好用的时间工具库,主要功能:

  • 当前时间
  • 修改地区
  • 解析字符串 等

Usage


import "github.com/jinzhu/now"

time.Now() // 2013-11-18 17:51:49.123456789 Mon

now.BeginningOfMinute()        // 2013-11-18 17:51:00 Mon
now.BeginningOfHour()          // 2013-11-18 17:00:00 Mon
now.BeginningOfDay()           // 2013-11-18 00:00:00 Mon
now.BeginningOfWeek()          // 2013-11-17 00:00:00 Sun
now.BeginningOfMonth()         // 2013-11-01 00:00:00 Fri
now.BeginningOfQuarter()       // 2013-10-01 00:00:00 Tue
now.BeginningOfYear()          // 2013-01-01 00:00:00 Tue

now.EndOfMinute()              // 2013-11-18 17:51:59.999999999 Mon
now.EndOfHour()                // 2013-11-18 17:59:59.999999999 Mon
now.EndOfDay()                 // 2013-11-18 23:59:59.999999999 Mon
now.EndOfWeek()                // 2013-11-23 23:59:59.999999999 Sat
now.EndOfMonth()               // 2013-11-30 23:59:59.999999999 Sat
now.EndOfQuarter()             // 2013-12-31 23:59:59.999999999 Tue
now.EndOfYear()                // 2013-12-31 23:59:59.999999999 Tue

now.WeekStartDay = time.Monday // Set Monday as first day, default is Sunday
now.EndOfWeek()                // 2013-11-24 23:59:59.999999999 Sun

修改地区

location, err := time.LoadLocation("Asia/Shanghai")

myConfig := &now.Config{
	WeekStartDay: time.Monday,
	TimeLocation: location,
	TimeFormats: []string{"2006-01-02 15:04:05"},
}

t := time.Date(2013, 11, 18, 17, 51, 49, 123456789, time.Now().Location()) // // 2013-11-18 17:51:49.123456789 Mon
myConfig.With(t).BeginningOfWeek()         // 2013-11-18 00:00:00 Mon

myConfig.Parse("2002-10-12 22:14:01")     // 2002-10-12 22:14:01
myConfig.Parse("2002-10-12 22:14")        // returns error 'can't parse string as time: 2002-10-12 22:14'

解析字符串

time.Now() // 2013-11-18 17:51:49.123456789 Mon

// Parse(string) (time.Time, error)
t, err := now.Parse("2017")                // 2017-01-01 00:00:00, nil
t, err := now.Parse("2017-10")             // 2017-10-01 00:00:00, nil
t, err := now.Parse("2017-10-13")          // 2017-10-13 00:00:00, nil
t, err := now.Parse("1999-12-12 12")       // 1999-12-12 12:00:00, nil
t, err := now.Parse("1999-12-12 12:20")    // 1999-12-12 12:20:00, nil
t, err := now.Parse("1999-12-12 12:20:21") // 1999-12-12 12:20:21, nil
t, err := now.Parse("10-13")               // 2013-10-13 00:00:00, nil
t, err := now.Parse("12:20")               // 2013-11-18 12:20:00, nil
t, err := now.Parse("12:20:13")            // 2013-11-18 12:20:13, nil
t, err := now.Parse("14")                  // 2013-11-18 14:00:00, nil
t, err := now.Parse("99:99")               // 2013-11-18 12:20:00, Can't parse string as time: 99:99`

参考

jefffff

Stay hungry. Stay Foolish COOL

Go backend developer

China Amoy