Golang中的一些小细节

1、结构体比较

golang中的结构体是可以直接比较的,底层调用了memequal函数来对内存进行比较,memequal是用汇编实现的。

 

2、方法接收者

如果一个类型实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。但是相反是不成立的。
例如:

package main

import "fmt"

type coder interface {
	code()
	debug()
}

type Gopher struct {
	language string
}

func (p Gopher) code() {
	fmt.Printf("I am coding %s language\n", p.language)
}

func (p *Gopher) debug() {
	fmt.Printf("I am debuging %s language\n", p.language)
}

func main() {
	var c coder = &Gopher{"Go"}
	c.code()
	c.debug()
}

在上面的接口中定义了两个方法,code和debug。然后定义了一个结构体,分别使用值接收者和指针接收者实现了code和debug方法,然后在main函数中,创建了一个Gopher的对象将其指针赋值给c,然后调用它的两个方法,是可以调用成功的。
运行结果:

I am coding Go language
I am debuging Go language

现在将main函数改一下:

func main() {
	var c coder = Gopher{"Go"}
	c.code()
	c.debug()
}

再次调用,会报错:

./main.go:23:6: cannot use Gopher literal (type Gopher) as type coder in assignment:
	Gopher does not implement coder (debug method has pointer receiver)

在上面的代码中将 &Gohper赋给coder是可以的,说明Gohper指针类型实现了code和debug方式;而将Gopher赋给coder会报错,说明Gopher值类型并没有实现这两个方法。

 

3、值传递

golang中只有值传递。例如,用切片作为函数的形参时也是值传递,切片底层的数据结构为:
定义在runtime/slice.go中

type slice struct {
	array unsafe.Pointer        // 底层数组的指针
	len   int                   // 数组长度
	cap   int                   // 数组容量
}

在reflect/value.go中有一个SliceHeader的结构体,它是slice的运行时表示,由于runtime中的slice是小写的,无法使用,因此可以使用reflect包中的SliceHeader。

// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
	Data uintptr                           // 底层数组的指针
	Len  int                               // 数组长度
	Cap  int                               // 数组容量
}

在传递参数时,相当于将实参的SliceHeader结构中的字段都拷贝到了形参里,它俩的Data指向同一块数据。

先看一段程序:

package main

import (
	"fmt"
)

func main() {

	s := []int{10, 20, 30}
	fmt.Println(s)

	changeSlice(s)

	fmt.Println(s)
}

func changeSlice(s []int) {
    
	for i := 0; i < 10; i++ {
		s = append(s, i)
	}

	fmt.Println(s)
}

// 运行结果
[10 20 30]
[10 20 30 0 1 2 3 4 5 6 7 8 9]
[10 20 30]

根据上面的程序可以看到,在changeSlice中对切片进行了修改,但是在main函数中打印的确实是没有修改的。我们可以猜测一下原因:因为在changeSlice函数中添加了数据,数组的空间不足,因此申请了新的空间,然后changeSlice中的s的Data指向了新的空间,但是在main函数中的s还是指向原来的空间,因此导致了man函数中的s还是原来的数据。如下图所示:

在这里插入图片描述

可以使用程序来证实一下:

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {

	s := []int{10, 20, 30}
	fmt.Println("--------main--------------")
	fmt.Println(s)
	fmt.Println(unsafe.Pointer(&s))
	sptr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
	fmt.Println(unsafe.Pointer(sptr.Data))



	changeSlice(s)

	fmt.Println(s)

}

func changeSlice(s []int) {
	fmt.Println("--------changeSlice--------------")
	fmt.Println(s)
	fmt.Println(unsafe.Pointer(&s))
	sptr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
	fmt.Println(unsafe.Pointer(sptr.Data))

	for i := 0; i < 10; i++ {
		s = append(s, i)
	}

	fmt.Println(s)
	fmt.Println(unsafe.Pointer(&s))
	sptr1 := (*reflect.SliceHeader)(unsafe.Pointer(&s))
	fmt.Println(unsafe.Pointer(sptr1.Data))
}

// 运行结果
--------main--------------
[10 20 30]
0xc000004078
0xc000014150
--------changeSlice--------------
[10 20 30]
0xc0000040a8
0xc000014150
[10 20 30 0 1 2 3 4 5 6 7 8 9]
0xc0000040a8
0xc000092000
[10 20 30]

可以看到,首先,main函数和changeSlice函数中的两个s的地址是不一样的,但是它们指向的底层的数据块的地址是一样的。而且在changeSlice函数中添加了数据之后,申请了新的内存。

 
但是如果我们提前设置足够的容量呢?那样就不会开辟新的数据,就算修改之后,两个s的Data指向的依然是同一块内存,因此在main函数打印修改后的数据应该是修改后的。事实真的是这样吗?

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {

	s := make([]int, 3, 13)
	s[0] = 10
	s[1] = 20
	s[2] = 30

	fmt.Println("--------main--------------")
	fmt.Println(s)
	fmt.Println(unsafe.Pointer(&s))
	sptr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
	fmt.Println(unsafe.Pointer(sptr.Data))

	changeSlice(s)

	fmt.Println(s)

}

func changeSlice(s []int) {
	fmt.Println("--------changeSlice--------------")
	fmt.Println(s)
	fmt.Println(unsafe.Pointer(&s))
	sptr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
	fmt.Println(unsafe.Pointer(sptr.Data))

	for i := 0; i < 10; i++ {
		s = append(s, i)
	}

	fmt.Println(s)
	fmt.Println(unsafe.Pointer(&s))
	sptr1 := (*reflect.SliceHeader)(unsafe.Pointer(&s))
	fmt.Println(unsafe.Pointer(sptr1.Data))
}

// 运行结果
--------main--------------
[10 20 30]
0xc000004078
0xc00001a0e0
--------changeSlice--------------
[10 20 30]
0xc0000040a8
0xc00001a0e0
[10 20 30 0 1 2 3 4 5 6 7 8 9]
0xc0000040a8
0xc00001a0e0
[10 20 30]

在上面的代码中,在创建切片时,就指定了容量为13 ,但是,结果出乎意料。可以看到,打印的依然是原来的数据。而且在changeSlice函数中添加了数据之后,指向的内存地址并没有改变,那么为什么打印的还是只有三个元素呢?

可以再来猜测一下,虽然底层数据是修改了,但是由于只在changeSlice中修改了s中的Len和Cap,而main函数中的s的Len和Cap并没有改变,因此虽然数据是添加成功了,但是main函数中的s的Len和Cap依然是3和13,因此就只打印了三个元素。如下图:

在这里插入图片描述

同样使用代码来证实一下:

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {

	s := make([]int, 3, 13)
	s[0] = 10
	s[1] = 20
	s[2] = 30

	fmt.Println("--------main--------------")
	fmt.Println(s)
	fmt.Println(unsafe.Pointer(&s))
	sptr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
	fmt.Println(unsafe.Pointer(sptr.Data))

	changeSlice(s)

	fmt.Println("--------main--------------")
	fmt.Println(s)
	fmt.Println(sptr.Len)
	fmt.Println(sptr.Cap)

	sptr.Len = 13
	fmt.Println(s)
}

func changeSlice(s []int) {
	fmt.Println("--------changeSlice--------------")
	fmt.Println(s)
	fmt.Println(unsafe.Pointer(&s))
	sptr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
	fmt.Println(unsafe.Pointer(sptr.Data))

	for i := 0; i < 10; i++ {
		s = append(s, i)
	}

	fmt.Println(s)
	fmt.Println(unsafe.Pointer(&s))
	fmt.Println(unsafe.Pointer(sptr.Data))

	fmt.Println(sptr.Len)
	fmt.Println(sptr.Cap)
}

// 运行结果
--------main--------------
[10 20 30]
0xc000004078
0xc00001a0e0
--------changeSlice--------------
[10 20 30]
0xc0000040a8
0xc00001a0e0
[10 20 30 0 1 2 3 4 5 6 7 8 9]
0xc0000040a8
0xc00001a0e0
13
13
--------main--------------
[10 20 30]
3
13
[10 20 30 0 1 2 3 4 5 6 7 8 9]

可以看到,在调用完changeSlice之后,main函数中的s的Len依然是3,但是底层的数据已经被改变了。因此手动将main函数中的s的Len改为13后也成功打印了预期的结果。这也是为什么go内置的append函数在追加了数据之后要返回一个切片的原因。
 
 

4、 string和[]byte之间相互转换

如果直接使用string()或[]byte()来进行类型转换是会进行内存的拷贝的,如果只是进行一次转换,转换后不会修改数据,那么就可以利用一些奇技淫巧来进行转换,从而得到更高的效率。我们知道,string和slice的底层都是一个结构体,因此在转换的时候,我们只需构造出这样的结构体,然后将Data指针指向的地址修改一下即可。
转换代码如下:

// string --> []byte
func String2ByteSlice(str string) []byte {
	return (*(*[]byte)(unsafe.Pointer(&struct {
		string
		Cap int
	}{str, len(str)})))
}

// 第二种方式 string --> []byte
func String2Bytes1(str string) []byte {
	x := (*[2]uintptr)(unsafe.Pointer(&str))
	ret := [3]uintptr{x[0], x[1], x[1]}
	return *(*[]byte)(unsafe.Pointer(&ret))
}

// []byte --> string
func ByteSlice2String(bts []byte) string {
	return *((*string)(unsafe.Pointer(&bts)))
}

基准测试的结果如下:
可以看到转换效率直接相差了一个数量级。
参考文章:你所知道的 string 和 []byte 转换方法可能是错的

goos: windows
goarch: amd64
pkg: code/string_slice_conv
cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz
BenchmarkByteSlice2String_Copy-8        339176784                3.499 ns/op
BenchmarkByteSlice2String_NoCopy-8      1000000000               0.2500 ns/op
BenchmarkString2ByteSlice_Copy-8        300531790                4.022 ns/op
BenchmarkString2ByteSlice_NoCopy-8      1000000000               0.2490 ns/op
PASS

 

5、类型重定义和类型别名

type关键字除了可以用于定义struct、interface之外,还可以根据已有的类型创建新的类型,还可以为已有类型创建类型别名:

// 定义新的类型
type MyInt int

// 类型别名
type Int = int

那么它们之间有什么区别呢?

  • 定义的新的类型与原类型是不同的,需要进行显式类型转换,而且新的类型不会拥有基类型的方法。
  • 类型别名与原类型的转换不需要显示类型转换,而且拥有基类型的方法。

类型重定义:

type User struct {
	Username string
}

func (u *User) String() string {
	return u.Username
}

type User1 User

func (u *User1) Print() {
	fmt.Println(u.Username)
}

func main() {
	u1 := User{}
	u2 := User1{}
	fmt.Printf("%T %T\n", u1, u2)
}

// 运行结果: 
main.User main.User1

而且可以看的,无法将u2直接赋值给u1,因为它们是不同的类型:
在这里插入图片描述
User中实现了String方法,但是基于User创建的User1类型确没有String方法:
在这里插入图片描述
 
类型别名:

type User struct {
	Username string
}

func (u *User) String() string {
	return u.Username
}


type User2 = User

func (u *User2) Print() {
	fmt.Println(u.Username)
}

func main() {
	u1 := User{}
	u2 := User2{}
	fmt.Printf("%T %T\n", u1, u2)
}

// 运行结果:
main.User main.User

可以看到输出的类型都是一样的。
 
可以直接进行赋值,而且给User2定义的方法,User的实例也可以使用:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值