描述
参加 Gopher China 2020 感觉所获颇丰,简单整理一下,共勉 …
代码解耦
- 旧代码
if len(req.FirstName) == 0 {
msg.Code = ErrFirstNameIsRequired
return
}
if len(req.LastName) == 0 {
msg.Code = ErrLastNameIsRequired
return
}
if len(req.Address) == 0 {
msg.Code = ErrAddressIsRequired
return
}
if len(req.PostalCode) == 0 {
msg.Code = ErrPostalCodeIsRequired
return
}
if len(req.City) == 0 {
msg.Code = ErrCityIsRequired
return
}
- 改动后
抽象一个方法
// ValidateString 字符串验证结构
type ValidateString struct {
Str string
Code int
}
// ValidateEmptyString 验证字符串是否为空
func ValidateEmptyString(s []*ValidateString) (code int, empty bool) {
for _, v := range s {
if len(v.Str) == 0 {
empty = true
code = v.Code
return
}
}
return
}
核心逻辑
// 参数合法性校验
code, empty := tools.ValidateEmptyString([]*tools.ValidateString{
{Str: req.FirstName, Code: ErrFirstNameIsRequired},
{Str: req.LastName, Code: ErrLastNameIsRequired},
{Str: req.Address, Code: ErrAddressIsRequired},
{Str: req.PostalCode, Code: ErrPostalCodeIsRequired},
{Str: req.City, Code: ErrCityIsRequired},
})
if empty {
msg.Code = code
return
}
说一下我的考虑,在一些层面看来这段代码改动甚至不能算是优化,它存在两个问题:
- 代码的行数并没有减少,反而增多了。
我认为一个好的优化,应该是在逻辑层看更简单易懂的逻辑,让代码看起来很清晰,减少重复性,将一些共性的东西抽象出来,其他位置有相同的逻辑可以进行复用,只要你的方法抽象合理
- 在调用方法时生成了很多临时变量,在
GC
的时候会增加扫描负担,影响性能。
我觉得这是一个个人取舍的问题,我对代码是有洁癖的,我不喜欢看到大量重复的恶心代码,哪怕会因此损失一小部分的性能,而且当服务的流量没有高到离谱的时候,这段代码对性能的影响微乎其微,如果真的到达了性能的瓶颈期,那是不是应该考虑下硬件资源是不是该加强下,或者架构上是否合理,当然很多人认为性能本身就是挤牙膏,那也无可厚非,至少我觉得写一手可读性高且美观的代码很重要,当然可能还有更好的方案,也欢迎沟通交流 …
Gorm 2.0 的新东西及注意事项
在我看来,在 Gorm 2.0 版本我们基本告别了 json.RawMessage 这个结构了、官方提供了自定义类型的方式,只需要实现两个方法:Scan 、 Value
一些对比
v1
定义结构
// Event 表
type Event struct {
ID string `gorm:"TYPE:TEXT;PRIMARY_KEY"` // ID
Info json.RawMessage `gorm:"TYPE:JSONB;DEFAULT:'{}'"` // 详细信息
CreatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 创建时间
UpdatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 更新时间
}
// Info 一些信息
type Info struct {
Detail string `json:"detail"`
}
抽象通用方法
// SetInfo 设置信息
func (e *Event) SetInfo(from string, info *Info) error {
newInfo, err := json.Marshal(info)
if err != nil {
logrus.Error(from+"SetInfo: ", err)
return err
}
e.Info = newInfo
return nil
}
// GetInfo 获取信息
func (e *Event) GetInfo(from string) *Info {
dbInfo := new(Info)
err := json.Unmarshal(e.Info, &dbInfo)
if err != nil {
logrus.Error(from+"GetInfo: ", err)
return nil
}
return dbInfo
}
落地使用
func main() {
e := new(Event)
// 获取信息
info := e.GetInfo("main")
if info == nil || string(e.Info) == "{}" {
logrus.Errorf("The info is empty [ %s ]", e.ID)
return
}
// 新增信息
err := e.SetInfo("main", &Info{
Detail: "",
})
if err != nil {
logrus.Errorf("e.SetInfo: [ %v ]", err)
return
}
// 入库
}
v2
定义结构
// Example 示例表
type Example struct {
ID string `gorm:"TYPE:TEXT;PRIMARY_KEY"` // ID
Info Info `gorm:"TYPE:JSONB;DEFAULT:'{}'"` // 详细信息
CreatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 创建时间
UpdatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 更新时间
}
// Info 一些信息
type Info struct {
Detail string `json:"detail"`
}
接口实现
// Scan 查询实现
func (b *Info) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
}
result := Info{}
err := json.Unmarshal(bytes, &result)
*b = result
return err
}
// Value 存储实现
func (b Info) Value() (driver.Value, error) {
return json.Marshal(b)
}
落地使用
func main() {
e := new(Event)
// 获取信息:数据库查询处理出来就已经处理好了
// 新增信息
e.Info = Info{
Detail: "",
}
// 入库
}
可以看到,落地使用的代码变得非常简捷,个人认为 Gorm 2.0 的自定义类型使项目本身的代码更加细致,虽然还是用程序去做 JOSN 解析而不是让数据库去做,但是可用性已经有了很大的提升,而且我个人也不是很倾向于让数据库去做这件事情,尽管他本身支持
自定义类型注意
我们如果想定义一个 JSON 数组、Map 的话,我们的处理方式就要改变一下
定义结构
type Example struct {
ID string `gorm:"TYPE:TEXT;PRIMARY_KEY"` // ID
Info Infos `gorm:"TYPE:JSONB;DEFAULT:'{}'"` // 详细信息
CreatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 创建时间
UpdatedAt time.Time `gorm:"DEFAULT:CURRENT_TIMESTAMP"` // 更新时间
}
// Info 一些信息
type Info struct {
Detail string `json:"detail"`
}
// Infos 一组信息
type Infos []Info
接口实现都是一样的,但是存储的时候要注意一个问题,就是不能直接存数组,因为你定义的类型 Infos
程序是认识的,但是并不认识 []Info
,所以如果你直接存 []info
会出现两种可能
- 数组中只有一个元素,存进去不是个数组而是对象,结果取出来的时候 JSON 反序列化失败。
// UpdateExample 测试更新示例
func TestUpdateExample(t *testing.T) {
id := "exampleID"
is := []Info{
{Detail: "example detail"},
}
err = UpdateExample(id, map[string]interface{}{
"infos": is,
})
if err != nil {
t.Fatal(err)
}
t.Log("Success")
}
- 数组中有两个元素,执行发生报错 ERROR: column “infos” is of type jsonb but expression is of type record (SQLSTATE 42804)
// UpdateExample 测试更新示例
func TestUpdateExample(t *testing.T) {
id := "exampleID"
is := []Info{
{Detail: "example detail 1"},
{Detail: "example detail 2"},
}
err = UpdateExample(id, map[string]interface{}{
"infos": is,
})
if err != nil {
t.Fatal(err)
}
t.Log("Success")
}
正确存储方式
// UpdateExample 测试更新示例
func TestUpdateExample(t *testing.T) {
id := "exampleID"
var is = Infos{}
is = []Info{
{Detail: "example detail 1"},
{Detail: "example detail 2"},
}
err = UpdateExample(id, map[string]interface{}{
"infos": is,
})
if err != nil {
t.Fatal(err)
}
t.Log("Success")
}
你必须先显式的指定你所存储的变量是你自定义的数据类型,存储才会是一个数组,否则只会把数据解析成对应结构的对象存储入库
常规类型注意
在 Gorm 2.0 中如果 string
、int
、time.Time
等类型,字段默认是 NULL
的话,扫描的时候会报错:converting NULL to (string/int …) is unsupported
- 解决方式一(使用已经定义好的数据库类型)
注意:使用这种方式存储的时候 Valid 字段必须显式的给出 True 才会存储,不然就会存储一个 null。
// Example 示例
type Example struct {
Str sql.NullString `gorm:"DEFAULT:NULL"` // 字符串
Int sql.NullInt64 `gorm:"DEFAULT:NULL"` // 数字
Bool sql.NullBool `gorm:"DEFAULT:NULL"` // 布尔
Float sql.NullFloat64 `gorm:"DEFAULT:NULL"` // 浮点
Time pq.NullTime `gorm:"DEFAULT:NULL"` // 时间
}
个人习惯这样使用
var (
str = "a string"
i int64
f float64
t = time.Now()
)
e := &Example{
Str: sql.NullString{String: str, Valid: len(str) > 0},
Int: sql.NullInt64{Int64: i, Valid: true},
Bool: sql.NullBool{Bool: true, Valid: true},
Float: sql.NullFloat64{Float64: f, Valid: true},
Time: pq.NullTime{Time: t, Valid: !t.IsZero()},
}
- 解决方式二:将默认值改为对应类型的零值
查询注意
在 Gorm 1.0 中我们可能会定义这样一种结构
// Example 示例
type Example struct {
Value sql.NullString `gorm:"DEFAULT:NULL"` // 表内字段
JoinValue string `gorm:"-"` // JOIN 表字段
}
在 Gorm 2.0 中 JoinValue
是不会查询到值的 -
这个标签被视为忽略读写,如果只期望查询而不存取的话现在应该使用内嵌,查询的时候还是查询 Example
表,Find
或 First
取值时使用 SelectExample
取值就可以了。
// SelectExample 查询结构
type SelectExample struct {
Example Example `gorm:"embedded"` // 表内字段
JoinValue string `gorm:"->"` // JOIN 表字段
}
// Example 示例
type Example struct {
Value sql.NullString `gorm:"DEFAULT:NULL"` // 表内字段
}
Gopher China 会议学习整理
主要还是一些 陈皓 在 2020 会议上讲得一些东西
Function VS Receive
习惯使用 Receiver 的方式
- Function
func PrintPerson(p *Person) {
fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",
p.Name, p.Sexual, p.Age)
}
func main() {
var p = Person{
Name: "Hao Chen",
Sexual: "Male",
Age: 44}
PrintPerson(&p)
}
- Receiver
func (p *Person) Print() {
fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",
p.Name, p.Sexual, p.Age)
}
func main() {
var p = Person{
Name: "Hao Chen",
Sexual: "Male",
Age: 44}
p.Print()
}
共性方法
主要目的还是抽离共性代码,避免重复代码出现
- 源码
type Country struct {
Name string
}
type City struct {
Name string
}
type Printable interface {
PrintStr()
}
func (c Country) PrintStr() {
fmt.Println(c.Name)
}
func (c City) PrintStr() {
fmt.Println(c.Name)
}
func main() {
c1 := Country{"China"}
c2 := City{"Beijing"}
c1.PrintStr()
c2.PrintStr()
}
- 优化
type WithName struct {
Name string
}
type Country struct {
WithName
}
type City struct {
WithName
}
type Printable interface {
PrintStr()
}
func (w WithName) PrintStr() {
fmt.Println(w.Name)
}
func main() {
c1 := Country{WithName{"China"}}
c2 := City{WithName{"Beijing"}}
c1.PrintStr()
c2.PrintStr()
}
验证接口是否被实现
- 接口定义及实现
type Shape interface {
Sides() int
Area() int
}
type Square struct {
len int
}
func (s *Square) Sides() int {
return 4
}
- 验证
var _ Shape = (*Square)(nil)
报错:
cannot use (*Square)(nil) (type *Square) as type Shape in assignment: *Square does not implement Shape (missing Area method)
性能对比
尽量使用 strconv 而不是 fmt
时间相差 78 ns +
- fmt
// 143 ns/op
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
}
- strconv
// 64.2 ns/op
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
}
避免 string to byte 的转换
时间相差 18 ns +
// 22.2 ns/op
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
}
// 3.25 ns/op
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
}
指定切片容量
时间相差 18 ns +
- 未指定容量
// 100000000 2.48s
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
- 指定容量
// 100000000 0.21s
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
使用 StringBuffer 或者 StringBuilder
时间相差 12 ns + ,不过这个差距看起来还是很明显的
- string +=
// 12.7 ns/op
var strLen int = 30000
var str string
for n := 0; n < strLen; n++ {
str += "x"
}
- StringBuilder
// 0.0265 ns/op
var strLen int = 30000
var builder strings.Builder
for n := 0; n < strLen; n++ {
builder.WriteString("x")
}
- StringBuffer
// 0.0088 ns/op
var strLen int = 30000
var buffer bytes.Buffer
for n := 0; n < strLen; n++ {
buffer.WriteString("x")
}