前言

匿名行为在go语言里非常常见,比如:

  • 匿名函数:也就是我们熟知的闭包(Closure)

  • 结构体里的匿名字段(Anonymous Fields)

  • 匿名结构体(Anonymous Structs)

匿名行为的设计带来了一些理解上的困难,但是熟悉了匿名设计的使用后,你会发现匿名设计在某些特定场景可以帮助大家写出更简洁、更优雅、更高效和更安全的代码。

什么是匿名结构体

匿名结构体:顾名思义,就是结构体没有命名。比如下面的代码示例:

// example1.go
package main

import (
	"fmt"
)

func main() {
	a := struct{name string; age int}{"bob", 10}
	b := struct{
		school string
		city string
	}{"THU", "Beijing"}
	fmt.Println(a, b)
}
复制代码

在这个例子里,我们定义了2个变量a和b,它们都是匿名结构体变量。

常见的使用场景

全局变量组合

有时候我们会在程序里定义若干全局变量,有些全局变量的含义是互相关联的,这个时候我们可以使用匿名结构体把这些关联的全局变量组合在一起。

// example2.go
package main

import "fmt"

// DBConfig 声明全局匿名结构体变量
var DBConfig struct {
	user string
	pwd string
	host string
	port int
	db string
}

// SysConfig 全局匿名结构体变量也可以在声明的时候直接初始化赋值
var SysConfig = struct{
	sysName string
	mode string
}{"tutorial", "debug"}

func main() {
	// 给匿名结构体变量DBConfig赋值
	DBConfig.user = "root"
	DBConfig.pwd = "root"
	DBConfig.host = "127.0.0.1"
	DBConfig.port = 3306
	DBConfig.db = "test_db"
	fmt.Println(DBConfig)
}
复制代码

对全局匿名结构体变量完成赋值后,后续代码都可以使用这个匿名结构体变量。

注意:如果你的程序对于某个全局的结构体要创建多个变量,就不能用匿名结构体了。

局部变量组合

全局变量可以组合,局部变量当然也可以组合了。

如果在局部作用域(比如函数或者方法体内)里,某些变量的含义互相关联,就可以组合到一个结构体里。

同时这个结构体只是临时一次性使用,不需要创建这个结构体的多个变量,那就可以使用匿名结构体。

// example3.go
package main

import "fmt"

func main() {
	// a和b作为局部匿名结构体变量,只是临时一次性使用
	// 注意:a是把struct里的字段声明放在同一行,字段之间要用分号分割,否则编译报错
	a := struct{name string; age int}{"Alice", 16}
	fmt.Println(a)

	b := struct{
		school string
		city string
	}{"THU", "Beijing"}
	fmt.Println(b)
}
复制代码

构建测试数据

匿名结构体可以和切片结合起来使用,通常用于创建一组测试数据。

// example4.go
package main

import "fmt"

// 测试数据
var indexRuneTests = []struct {
	s    string
	rune rune
	out  int
}{
	{"a A x", 'A', 2},
	{"some_text=some_value", '=', 9},
	{"☺a", 'a', 3},
	{"a☻☺b", '☺', 4},
}

func main() {
	fmt.Println(indexRuneTests)
}
复制代码

嵌套锁(embeded lock)

我们经常遇到多个goroutine要操作共享变量,为了并发安全,需要对共享变量的读写加锁。

这个时候通常需要定义一个和共享变量配套的锁来保护共享变量。

匿名结构体和匿名字段相结合,可以写出更优雅的代码来保护匿名结构体里的共享变量,实现并发安全。

// example5.go
package main

import (
	"fmt"
	"sync"
)

// hits 匿名结构体变量
// 这里同时用到了匿名结构体和匿名字段, sync.Mutex是匿名字段
// 因为匿名结构体嵌套了sync.Mutex,所以就有了sync.Mutex的Lock和Unlock方法
var hits struct {
	sync.Mutex
	n int
}

func main() {
	var wg sync.WaitGroup
	N := 100
	// 启动100个goroutine对匿名结构体的成员n同时做读写操作
	wg.Add(N)
	for i:=0; i<100; i++ {
		go func() {
			defer wg.Done()
			hits.Lock()
			defer hits.Unlock()
			hits.n++
		}()
	}
	wg.Wait()
	fmt.Println(hits.n) // 100
}
复制代码

HTTP处理函数中JSON序列化和反序列化

我们在处理http请求时,通常会和JSON数据打交道。

比如post请求的content-type使用application/json时,服务器接收过来的json数据是key:value格式,不同key的value的类型可以不一样,可能是数字、字符串、数组等,因此会遇到使用json.Unmarshalmap[string]interface{}来接收JSON反序列化后的数据。

但是使用map[string]interface{}有几个问题:

  • 没有类型检查:比如json的某个value本来预期是string类型,但是请求传过来的是bool类型,使用json.Unmarshal解析到map[string]interface{}是不会报错的,因为空interface可以接受任何类型数据。

  • map是模糊的:Unmarshal后得到了map,我们还得判断这个key在map里是否存在。否则拿不存在的key的value,得到的可能是给nil值,如果不做检查,直接对nil指针做*操作,会引发panic。

  • 代码比较冗长:得先判断key是否存在,如果存在,要显示转换成对应的数据类型,并且还得判断转换是否成功。代码会比较冗长。

这个时候我们就可以使用匿名结构体来接收反序列化后的数据,代码会更简洁。参见如下代码示例:

// example6.go
// 请求命令:curl -X POST -H "content-type: application/json" http://localhost:4000/user -d '{"name":"John", "age":111}'
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

func AddUser(w http.ResponseWriter, r *http.Request) {
	// data 匿名结构体变量,用来接收http请求发送过来的json数据
	data := struct{
		Name string `json:"name"`
		Age int	`json:"age"`
	}{}
	// 把json数据反序列化到data变量里
	decoder := json.NewDecoder(r.Body)
	err := decoder.Decode(&data)
	if err != nil {
		fmt.Println("error:", err)
	}
	fmt.Println(data)
	fmt.Fprint(w, "Hello!")
}

func index(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "index")
}

func main() {
	http.HandleFunc("/", index)
	http.HandleFunc("/user", AddUser)
	log.Fatal(http.ListenAndServe("localhost:4000", nil))
}
Copy the code

conclusion

Anonymous structs allow us to define struct variables without first defining struct types. Allow structure definitions and variable definitions to be combined in one go.

Anonymous constructs have the following application scenarios:

  • Combined variables:

    • Global anonymous structure: Groups together related global variables

    • Local anonymous structure: temporary, disposable

  • Build test data:

    • Anonymous structure + slice, construct a group of test data
  • Nested locks: More elegant code that combines the shared access data of multiple Goroutines and locks that protect shared data in an anonymous structure.

  • Json serialization and deserialization in HTTP handler functions:

    • Anonymous structure variables are used to receive HTTP request data

    • The code is simpler and more secure than map[string]interface{}

Open source address

Documentation and code: github.com/jincheng9/g…

We also welcome you to pay attention to the public account: Coding to learn more about Go, micro services and cloud native architecture.

References

  • Go. Dev/talks / 2012 /…

  • Qvault. IO/golang/anon…