gorm 深度剖析using unaddressable value

使用gorm的时候遇到一个问题,想往一个设置了主键自增的表里加数据,心想主键既然是自增,那结构体赋值的时候主键那个变量空着就好了吧,于是乎我是这么写的:

		customer = Customer{
			Name:    name,
			Age:     age,
			Address: address,
			Phone:   phone,
			Score:   score,
		}

		if err := db.Create(customer).Error; err != nil {
			fmt.Println("插入失败")
			fmt.Println("error is: ", err)
			return false
		}

其中,Customer结构体长这样:

type Customer struct {
	Id int `grom:"AUTO_INCREMENT"`
	Name string `grom:"NOT NULL"`
	Address string
	Phone string `gorm:"NOT NULL,unique_index"`
	Age int
	Score int `grom:"DEFAULT:0"`
	//born year
}

你应该发现了,Customer结构体里,Id就是我们设置的自增的主键,而在程序初始化这个结构体的时候,我并没有给Id赋值,当然,go语言会自动把Id赋值为0。欧克,就这么运行一下吧。

结果出问题了:

使用了无法取地址的值,什么鬼?

百度了一下,相关的帖子有五六个,都是说我们在该传指针的地方传了值,也就是说,参数传结构体指针进去就行了,于是乎,改成下面这样:

db.Create(&customer)

再一运行,果然没有问题了。

大多数人到这里就长呼一口气,问题解决,啦啦啦。。。

但是,身边的一个初学go的C语言大佬提出了疑问,她是这么写的:

恩,这种写法,参数还是传的结构体值,只不过,定义结构体的时候给Id赋了一个非0初值,然后结果是,插入成功了。

这该如何解释?只要给Id赋了初值,传值进去也可以???

作为一个写了20多篇go语言相关文章、将go语言作为本命语言的人,这个问题要是解释不出来,颜面扫地啊。。。

于是乎,硬着头皮,调试了一波gorm源码,最终,靠实力挽回了颜面,哈哈。

using unaddressable value是哪里报出来的?

我的思路是,要解决这个using unaddressable value报错,先找到源码里这个错误是在哪报出来的。

然而这个东西,我真的是找了好久。。。

首先这个error是Create函数返回的,那我们就先看Create函数吧:

func (s *DB) Create(value interface{}) *DB {
	scope := s.NewScope(value)
	return scope.callCallbacks(s.parent.callbacks.creates).db
}

不得不说,封装的真好啊。。。恩,反正就是到这啥也看不出来。。。

不过还是要先记着,这里定义了一个scope变量,scope,翻译过来是范围,所以这一行就是把传进来的结构体变成了一个范围???什么鬼。。。先不管,反正就是结构体放到scope里了,然后对scope进行了一个callCallbacks操作,那么数据肯定是在这个callCallbacks函数里存进数据库的,所以我们再瞅瞅这个callCallbacks函数:

func (scope *Scope) callCallbacks(funcs []*func(s *Scope)) *Scope {
	defer func() {
		if err := recover(); err != nil {
			if db, ok := scope.db.db.(sqlTx); ok {
				db.Rollback()
			}
			panic(err)
		}
	}()
	for _, f := range funcs {
		(*f)(scope)
		if scope.skipLeft {
			break
		}
	}
	return scope
}

这里有两块,先是定义了一个defer,里面recover了panic,按照代码的运行结果,应该没有走到这里。

另一块,在一个for循环里,执行了一堆函数,哪堆函数呢?这个funcs是作为参数传进来的,而刚刚Create函数里调用这个函数的时候,传进来的是s.parent.callbacks.creates,这个东西找到定义处一看,是这样的:

恩,就是个函数指针数组,那么这个数组又是在什么时候赋值的呢???

阿西吧,这么一层一层的看,得看到啥时候去。。。能不能不全都看了就知道那些函数到底是什么呢?这时我想起来,可以调试啊!在想看的地方加个断点,那个地方的变量是啥不就都知道了。

欧克,既然我想知道的是funcs里都是哪些函数,那就在这先来一个断点吧。

卧槽,这玩意儿到这运行了9个函数。。。

看函数名,其实可以大概知道分别是在干什么,大概就是准备,然后执行,然后提交或者回滚吧,还有几个说不清楚的,先不管了,不要偏离我们的第一步目标,就是找到error到底是哪里报出来的

为此,我们先定位,哪个函数执行完出现的error,于是乎再加一个断点:

然后,就一个一个看吧:

终于,找到了,在createCallback这个函数执行完,error哪里从nil变成了using unaddressable value。

接下来,就到createCallback这个函数里面去看看,到底哪错了。

createCallback这个函数略有些长,我就不全贴过来了,反正呢,就是加一堆断点,看看到哪error从nil变成using unaddressable value,最终发现是在这里:

也就是说,在执行primaryField.Set(primaryValue)的时候,看这句的意思,应该是在给主键赋值。恩,这个说的通,因为问题就是在于那个我们没有赋值的自增主键,接下来就进这个Set函数看看,具体怎么出的错。

天哪,好像就是这里,这块对field字段执行了CanAddr方法,这函数看名字就是,判断变量是否可以取地址,再看下面返回的这个ErrUnaddressable:

是没错,就是这里了。

接下来,我们再反过去,一步一步分析,事情,是怎么发展到如今这一步的。。。

CanAddr为什么返回false?

错误是在field.Field.CanAddr()返回false的情况下产生的,所以我们首先要知道,为何返回false。

func (v Value) CanAddr() bool {
	return v.flag&flagAddr != 0
}
这个函数好简单,我喜欢。。。这里面呢,判断了v.flag和flagAddr相与的结果,如果是0就是返回false,那么问题就来了,v是啥?flagAddr又是啥?

flagAddr这个简单:

看到这我们可以大概推测出来了,v.flag就是字段的一个属性,flag的每一个二进制位都有对应的含义,其中第9位,就用于标识一个字段是不是可以取地址。

那么接下来的思路就是,找到v.flag的第9位是在什么时候、按照什么规则修改的。

flag是在哪设置的?怎么设置的?

这个,我又搞了很久。。。

首先,flag是哪来的?可以看一下这里的断点:

恩,就是说flag是field下面的Field下面的一个变量,而field是个结构体,这个结构体又是哪里来的呢,我们回到调用Set的地方:

这是createCallback函数里调用Set的地方,这里我们发现,调用Set方法的结构体是primaryField,那么这个结构体又是哪里定义的呢?这个好找,在这里:

看到这我又跪了,又是调用函数返回的。。。这里面一定又是一层套一层的函数。。。

没办法,一个一个看吧,先看这个PrimaryField函数,看这个名字,就是从所有字段里面找主键字段:

func (scope *Scope) PrimaryField() *Field {
	if primaryFields := scope.GetModelStruct().PrimaryFields; len(primaryFields) > 0 {
		if len(primaryFields) > 1 {
			if field, ok := scope.FieldByName("id"); ok {
				return field
			}
		}
		return scope.PrimaryFields()[0]
	}
	return nil
}

这里面呢,首先看第一行,看这个意思呢,可能没有,可能有一个,也可能有多个,如果有多个,就找名字叫id的那个,如果没有叫id的字段,就返回第一个,这里又调用了scope.PrimaryFields,这东西一开始看我以为跟外面函数同名,我还想这难道是个递归?结果仔细一看,发现并不是。。。后面多了个s。。。不说了,看吧:

func (scope *Scope) PrimaryFields() (fields []*Field) {
	for _, field := range scope.Fields() {
		if field.IsPrimaryKey {
			fields = append(fields, field)
		}
	}
	return fields
}

这个呢,看意思就是把所有的字段遍历一遍,如果是主键,就放到fields里,然后返回所有主键的slice,这里面其实还有一个函数,scope.Fields(),不看这个你也不知道所有字段是从哪来的:

func (scope *Scope) Fields() []*Field {
	if scope.fields == nil {
		var (
			fields             []*Field
			indirectScopeValue = scope.IndirectValue()
			isStruct           = indirectScopeValue.Kind() == reflect.Struct
		)

		for _, structField := range scope.GetModelStruct().StructFields {
			if isStruct {
				fieldValue := indirectScopeValue
				for _, name := range structField.Names {
					if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
						fieldValue.Set(reflect.New(fieldValue.Type().Elem()))
					}
					fieldValue = reflect.Indirect(fieldValue).FieldByName(name)
				}
				fields = append(fields, &Field{StructField: structField, Field: fieldValue, IsBlank: isBlank(fieldValue)})
			} else {
				fields = append(fields, &Field{StructField: structField, IsBlank: true})
			}
		}
		scope.fields = &fields
	}

	return *scope.fields
}

这个函数呢,这里我们先不说,为啥呢,请看函数第一句,判断scope.fields是不是空,如果不是,就啥也不做,我加断点继续调试到这里的时候,发现这里scope.fields已经不是空了,也就是说,前面已经有地方执行了这个,所以我们要找第一次调用这个函数的地方,因为是在那里生成的这个field。

怎么找呢?很简单,接着加断点,这个field是scope结构体的一个成员,而scope是在Create函数里创建的,所以我们就从Create开始观察这个field,看看什么时候,它从nil变成不是nil的,那里就是第一次调用这个函数的地方。

field什么时候写入的?

我们再重新执行一遍程序,还是看callCallback里面,那九个函数,执行到第几个,field不是nil的。

找到了,是这个saveBeforeAssociationsCallback,这个函数执行完,field变成了一个长度为6的切片,这正好对应着结构体里的6个变量,同时也就是数据库表里的6个字段。

接下来,就是看,saveBeforeAssociationsCallback里面怎么写入的这个field。

这个函数略长,不过不重要,我们关心的是这个函数什么地方写入的field,这个直接公布答案:

是的,就在函数第一行,scope.Fields(),眼熟不,这就是我们之前说过的那个生成field的函数,在这里,field还是nil,所以field就是在这个时候,调用了scope.Fields(),然后在Fields函数里写入的,现在我们再来仔细看这个Field函数的逻辑。

		var (
			fields             []*Field
			indirectScopeValue = scope.IndirectValue()
			isStruct           = indirectScopeValue.Kind() == reflect.Struct
		)

		for _, structField := range scope.GetModelStruct().StructFields {
			if isStruct {
				fieldValue := indirectScopeValue
				for _, name := range structField.Names {
					if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
						fieldValue.Set(reflect.New(fieldValue.Type().Elem()))
					}
					fieldValue = reflect.Indirect(fieldValue).FieldByName(name)
				}
				fields = append(fields, &Field{StructField: structField, Field: fieldValue, IsBlank: isBlank(fieldValue)})
			} else {
				fields = append(fields, &Field{StructField: structField, IsBlank: true})
			}
		}
		scope.fields = &fields

我们从那个for循环开始看,这个循环,很显然,就是依次将结构体里的所有成员,转化成field然后存到scope的field里面,这里程序首先判断scope的value是不是一个结构体,如果不是,我们就不考虑这个情况了。。。不管我们传进来的是结构体,还是结构体指针,都会满足这个isStruct为true,为啥呢?结构体指针和结构体在go里面算一种类型吗?带着这个疑问我们来看一看isStruct是怎么赋值的,在上面那个定义变量那里:

isStruct是不是为true,看的是indirectScopeValue的类型是不是Struct,而indirectScopeValue是上一个定义的变量,这个变量的赋值,又是一个函数的返回值。。。唉,到这里我已经麻痹了,函数就函数,看起:

func (scope *Scope) IndirectValue() reflect.Value {
	return indirect(reflect.ValueOf(scope.Value))
}

这。。。为啥不直接调用里面这个呢。。。

func indirect(reflectValue reflect.Value) reflect.Value {
	for reflectValue.Kind() == reflect.Ptr {
		reflectValue = reflectValue.Elem()
	}
	return reflectValue
}

这个,嘿嘿,可以看懂了吧,不可以你可以继续点下去,我这里就不写那么细了,这块呢,就是判断Create那里传进来的参数,是不是指针,如果是,转化为指针指向的值,所以说,不管传进来的是结构体值还是结构体指针,都是可以处理滴~

到这块,最开始我们的疑惑你应该已经有一些眉目了,也就是说,Create那里并不一定要传结构体指针进来。

我们继续看,这里我们想确认的是,这里既然都转化成值了,那么在CanAddr那里,怎么知道这个field是值还是指针的呢?

为了搞清楚这个,我们还得接着看下去,看看将指针转化成值的那个函数,就是reflectValue.Elem():

这个函数,支持把接口或者指针转化成对应的值,我们已经知道是要把指针转化为值了,所以接口那个case就不管了先。。。

关键的地方,我已经红框框圈起了,怎么样,是不是已经有豁然大明白的感觉了?

fl就是field里面那个flag,而这个fl,在这里,通过与flagAddr相与,flag的第9位被置为了1,怎么样,和上面呼应上了吧,嘿嘿。

因此,如果我们Create那里如果传的是结构体指针,在写入field的时候,flag就会带上这个指针属性,到createCallback那里面调用Set函数设置主键值的时候,就不会报using unaddressable value这个错误了。

为什么Id赋值成非零值的时候,传值也可以?

上面只搞清楚了一个问题,就是为什么传结构体指针可以,但是还没有搞清楚另一个问题,就是为什么把Id赋值成一个非0值,然后传结构体值进来也没问题,为了搞明白这个问题,我们在会带createCallback那个函数去。

这是我们调用Set的地方,刚才也说了,using unaddressable value就是在Set函数里报出来的,但是要注意的是,并不是所有情况,都需要调用Set,看到上面那个红框框了吧,如果那个IsBlank不满足,是不会调用到Set的,那么问题又来了,什么时候,field的IsBlank属性是false呢?

这里我们又要跳到field写入的地方去看了,是的,又是那个scope.Fields()函数,注意里面的这一句:

看到了吧,每个字段的IsBlank就是在这里设置的,就下来我们就可以看这个isBlank函数的实现:

怎么样,是不是又豁然大明白了。只要是整数类型,值为零,那IsBlank就是true,而我们的主键Id,正是一个int类型。当我们把Id手动赋值为非0值的时候,这里isBlank的结果就是false,再回到createCallback那里,IsBlank是false,就不会执行那个Set,也就不会判断传进来的参数是不是指针,也就不会报那个using unaddressable value了。

到这里,所有谜团全部告破,终于可以长舒一口气了~

总结

看我上面这段分析的话,其实也不是太复杂,但这个问题我确实是花了大半天才搞定,过程中做了很多无用功,究其原因还是经验不足啊,说起来,这应该是我第一次看go语言的源码,源码还是比我想象的要复杂的多啊,如此看来,想写一个大家都愿意用的好程序,还真是要花很大功夫。

其实刚开始看的时候,这玩意儿一个函数套着另一个函数,一个结构体套着另一个结构体的,真是把我搞得晕头转向,好几次都想放弃了,觉得自己根本不可能看懂,好在每次想放弃的时候,都有一股莫名的力量(可能是想成为一名月入百万的程序员的梦想吧)支撑我坚持了下去。

最后想对自己,也对所有人说一句,遇到问题,一定要因难而上,不要又怎么能有所提升呢,对吧,哈哈,不多哔哔了,一起努力吧!

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值