空对象

空对象是个神奇的东西。它指的是没有字段的结构类型。

1
type Q struct{}

不占用空间,长度为0

1
2
  var s struct{}
  fmt.Println(unsafe.Sizeof(s)) // prints 0

如果结构体中仅有一个空结构字段,那么结构体的对齐字节数为1

1
2
  	var s struct{}
	fmt.Println(unsafe.Alignof(s))

由于空结构体占用0字节,那么空结构体也不需要填充字节。所以空结构体组成的组合数据类型也不会占用内存空间。

1
2
3
4
5
6
type S struct {
        A struct{}
        B struct{}
}
var s S
fmt.Println(unsafe.Sizeof(s)) // prints 0

可以和普通结构一样操作

由于Go的正交性,空结构体可以像其他结构体一样正常使用。正常结构体拥有的属性,空结构体一样具有。

你可以定义一个空结构体组成的数组,当然这个切片不占用内存空间。

1
2
var x [1000000000]struct{}
fmt.Println(unsafe.Sizeof(x)) // prints 0

空结构体组成的切片的宽度只是他的头部数据的长度,就像上例展示的那样,切片元素不占用内存空间。

1
2
var x = make([]struct{}, 1000000000)
fmt.Println(unsafe.Sizeof(x)) // prints 12 in the playground

当然切片的内置子切片、长度和容量等属性依旧可以工作。

1
2
3
var x = make([]struct{}, 100)
var y = x[:50]
fmt.Println(len(y), cap(y)) // prints 50 100

声明两个空对象,它们指向同一个地址(错误)

1
2
3
4
  type A struct{}
  a := A{}
  b := A{}
  fmt.Println(&a == &b) // prints true

造成这个结果的原因是 Golang 的编译器会把这种空对象都当成runtime.zerobase处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var zerobase uintptr
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    if gcphase == _GCmarktermination {
  	throw("mallocgc called with gcphase == _GCmarktermination")
    }

    if size == 0 {
  	return unsafe.Pointer(&zerobase)
    }
    ...
}

从 Go1.6 开始输出的结果有了变化,之前是true,现在是false。

官方的解释:

Pointer values are comparable. Two pointer values are equal if they point to the same variable or if both have value nil. Pointers to distinct zero-size variables may or may not be equal.

不过这个处理并不影响对于空结构体的使用。官方对于空指针相等的判断进行了修改,至于原因并没有给出详细的解释。

hashset

实现

有了上面的介绍,就可以利用空结构来优化 hashset 了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var itemExists = struct{}{}

type Set struct {
	items map[interface{}]struct{}
}

func New() *Set {
	return &Set{items: make(map[interface{}]struct{})}
}

func (set *Set) Add(item interface{}) {
	set.items[item] = itemExists
}

func (set *Set) Remove(item interface{}) {
	delete(set.items, item)
}

func (set *Set) Contains(item interface{}) bool {
	if _, contains := set.items[item]; !contains {
		return false
	}
	return true
}

一个简易的 hashset 实现就完成了。

性能比较

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func BenchmarkIntSet(b *testing.B) {
	var B = NewIntSet(3)
	B.Set(10).Set(11)
	for i := 0; i < b.N; i++ {
		if B.Exists(1) {

		}
		if B.Exists(11) {

		}
		if B.Exists(1000000) {

		}
	}
}

func BenchmarkMap(b *testing.B) {
	var B = make(map[int]int8, 3)
	B[10] = 1
	B[11] = 1
	for i := 0; i < b.N; i++ {
		if _, exists := B[1]; exists {

		}
		if _, exists := B[11]; exists {

		}
		if _, exists := B[1000000]; exists {

		}
	}
}
1
2
BenchmarkIntSet-2       50000000                35.3 ns/op             0 B/op          0 allocs/op
BenchmarkMap-2          30000000                41.2 ns/op             0 B/op          0 allocs/op

结论

性能,有些提升,但不是特别明显。尤其是线上压力不大的情况性能应该不会有明显变化; 内存占用。我们的服务缓存较多、占用内存较大,通过这个优化实测可以减少 1.6 GB 的空间。不过这个优化的空间取决于数据量。

事件通知用Channel

如果channel不用来传递数据,而是用作事件通知,可以用chan struct{}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main(){
    done:=make(chan struct{})
    c:=make(chan string)

    go func(){
        s:=<-c
        println(s)
        close(done)
    }()
    c<-"hi!"
    <-done
}

参考

http://blog.cyeam.com/golang/2017/04/11/go-empty-struct