Golang那些违背直觉的编程陷阱

目录

知识点1:切片拷贝之后都是同一个元素

知识点2:方法集合决定接口实现,类型方法集合是接口方法集合的超集则认定为实现接口,否则未实现接口


切片拷贝之后都是同一个元素
package mainimport ("encoding/json""fmt"
)func arr1() {var nn []intfor i := 0; i < 5; i++ {nn = append(nn, i)}marshal, _ := json.Marshal(nn)// [0,1,2,3,4]fmt.Println(string(marshal))
}func arr2() {var nn []*intfor i := 0; i < 5; i++ {// 出错原因:每次都是i的地址,i的地址始终是一个,所以最终数组元素是5// 解决方法,每次新生命一个变量,之后使用每次新分配的变量进行赋值。参见arr3nn = append(nn, &i)}//[5,5,5,5,5]marshal, _ := json.Marshal(nn)fmt.Println(string(marshal))
}func arr3() {var nn []*intfor i := 0; i < 5; i++ {// s := inn = append(nn, &s)}marshal, _ := json.Marshal(nn)//[0,1,2,3,4]fmt.Println(string(marshal))
}func main() {arr1()arr2()arr3()
}

主要看一下arr2与arr3函数即可知晓,很好理解却又很容易疏忽。接下来看一个类似问题的变种,跟struct方法有关系示例:

package mainimport ("fmt""time"
)type field struct {name string
}func (p *field) print() {fmt.Println(p.name)
}func main() {data1 := []*field{{"one"}, {"two"}, {"three"}}for _, v := range data1 {go v.print()}data2 := []field{{"four"}, {"five"}, {"six"}}for _, v := range data2 {go v.print()}time.Sleep(3 * time.Second)
}

| 这个代码执行输出结果:

看到结果是不是很意外,为什么有3个six呢?接下来分下一下:由于field的print方法是指针类型,所以data2每次在调用print方法时都是v指向的内存对象,这个对象最后一次赋值是six,所以输出的是3个six(其实此处存在不确定性,main协程与子协程的调度顺序,如果每次调度main协程之后立马就去调度子协程可能结果就是正确的了)。

那怎么修复问题呢?

方法1:

将filed的print方法的接受者修改为值类型,这样每次调用时都会拷贝一个副本进行调用,就会背面这个问题了,具体如下:

func (p field) print() {fmt.Println(p.name)
}

方法2:

每次调用时重新声明一个变量进行调用,这个底层原理也是拷贝一个副本进行调用,具体修改如下:

package mainimport ("fmt""time"
)type field struct {name string
}func (p *field) print() {fmt.Println(p.name)
}func main() {data1 := []*field{{"one"}, {"two"}, {"three"}}for _, v := range data1 {go v.print()}data2 := []field{{"four"}, {"five"}, {"six"}}for _, v := range data2 {replica := v// 此处每次都是重新分配一个内存存储v的副本go replica.print()}time.Sleep(3 * time.Second)
}
方法集合决定接口实现,类型方法集合是接口方法集合的超集则认定为实现接口,否则未实现接口
package mainimport ("fmt""reflect"
)type Interface interface {M1()M2()
}type T struct{}func (t T) M1()  {}
func (t *T) M2() {}func DumpMethodSet(i interface{}) {v := reflect.TypeOf(i)elemTyp := v.Elem()n := elemTyp.NumMethod()if n == 0 {fmt.Printf("%s's method set is empty!\n", elemTyp)return}fmt.Printf("%s's method set:\n", elemTyp)for j := 0; j < n; j++ {fmt.Println("-", elemTyp.Method(j).Name)}fmt.Printf("\n")
}
func main() {var t Tvar pt *Tvar i Interface//Cannot use 't' (type T) as type Interface//Type does not implement 'Interface' as 'M2' method has a pointer receiver// 言外之意就是类型T没有实现接口的M2方法i = ti = pt
}

此处主要需要了解Go方法集合规范是什么才能更好解释问题。如下工具方法可以用于查看类型的方法集合,具体代码如下:

func DumpMethodSet(i interface{}) {v := reflect.TypeOf(i)elemTyp := v.Elem()n := elemTyp.NumMethod()if n == 0 {fmt.Printf("%s's method set is empty!\n", elemTyp)return}fmt.Printf("%s's method set:\n", elemTyp)for j := 0; j < n; j++ {fmt.Println("-", elemTyp.Method(j).Name)}fmt.Printf("\n")
}

调用:

var t T
var pt *T
DumpMethodSet(&t)
DumpMethodSet(&pt)
DumpMethodSet((*Interface)(nil))

输出:

因为T类型的方法集合只有M1,所以导致上面将T类型实例赋值给接口类型会报错。

重点:Golang方法集合规范

1. 对于非接口类型的自定义类型T,其方法集合由所有receiver为T类型的方法组成;

2. 而类型*T的方法集合则包含所有receiver为T和*T类型的方法。也正因为如此,pt才能成功赋值给Interface类型变量。

特别提示:在进行组合时候,内嵌的是指针或值类型的结构体所以涉及引入的方法集是不一样的,也遵循上面规范。一般来说内嵌指针的方法集大于等于值得方法集。参见代码:

package mainimport "51788.net/golang-day01/dump_method_set"//main.T1's method set:
//- T1M1
//- T1M2//*main.T1's method set:
//- PT1M3
//- T1M1
//- T1M2
type T1 struct{}func (T1) T1M1()   { println("T1's M1") }
func (T1) T1M2()   { println("T1's M2") }
func (*T1) PT1M3() { println("PT1's M3") }//main.T2's method set:
//- T2M1
//- T2M2
//
//*main.T2's method set:
//- PT2M3
//- T2M1
//- T2M2
type T2 struct{}func (T2) T2M1()   { println("T2's M1") }
func (T2) T2M2()   { println("T2's M2") }
func (*T2) PT2M3() { println("PT2's M3") }//main.T's method set:
//- PT2M3
//- T1M1
//- T1M2
//- T2M1
//- T2M2
//
//*main.T's method set:
//- PT1M3
//- PT2M3
//- T1M1
//- T1M2
//- T2M1
//- T2M2
type T struct {T1*T2
}func main() {t := T{T1: T1{},T2: &T2{},}pt := &tvar t1 T1var pt1 *T1dump_method_set.DumpMethodSet(&t1)dump_method_set.DumpMethodSet(&pt1)var t2 T2var pt2 *T2dump_method_set.DumpMethodSet(&t2)dump_method_set.DumpMethodSet(&pt2)dump_method_set.DumpMethodSet(&t)dump_method_set.DumpMethodSet(&pt)
}

结论:

  • T类型的方法集合 = T1的方法集合 + *T2的方法集合;
  • *T类型的方法集合 = *T1的方法集合 + *T2的方法集合。
接口方法覆盖
package mainimport "51788.net/golang-day01/dump_method_set"type Interface1 interface {M1()
}type Interface2 interface {M1()M2()
}type Interface3 interface {Interface1Interface2 // Go 1.14之前版本报错:duplicate method M1
}type Interface4 interface {Interface2M2() // Go 1.14之前版本报错:duplicate method M2
}func main() {dump_method_set.DumpMethodSet((*Interface3)(nil))
}

在golang1.14版本之后允许接口中相同方法的覆盖。

类型里面内嵌多个接口,多个接口方法集合存在交集

当多个接口方法存在交集时,交集方法必须在类型上进行显示实现,否则调用交集方法时会报错。(当然如果不显示实现,而且后续不调用交集方法的话也不会报错。如果使用交集方法就要一定在类型上实现交集方法)。

示例1:

package mainimport "fmt"type IRun1 interface {M1()M2()
}type IRun2 interface {M2()M3()
}type IRun1Impl struct{}func (IRun1Impl) M1() {fmt.Println(" (IRun1Impl) M1()")
}func (IRun1Impl) M2() {fmt.Println(" (IRun1Impl) M2()")
}type IRun2Impl struct{}func (IRun2Impl) M2() {fmt.Println(" (IRun2Impl) M2()")
}func (IRun2Impl) M3() {fmt.Println(" (IRun2Impl) M3()")
}type TRun struct {IRun1IRun2
}func (e TRun) M1() {fmt.Println("t m1")
}// 一定在类型上实现交集方法
func (e TRun) M2() {fmt.Println("t m2")
}func main() {e := TRun{IRun1: &IRun1Impl{},IRun2: &IRun2Impl{},}e.M1()e.M2()e.M3()//	输出://t m1//t m2// (IRun2Impl) M3()
}

示例2:

package mainimport "fmt"type IRun1 interface {M1()M2()
}type IRun2 interface {M2()M3()
}type IRun1Impl struct{}func (IRun1Impl) M1() {fmt.Println(" (IRun1Impl) M1()")
}func (IRun1Impl) M2() {fmt.Println(" (IRun1Impl) M2()")
}type IRun2Impl struct{}func (IRun2Impl) M2() {fmt.Println(" (IRun2Impl) M2()")
}func (IRun2Impl) M3() {fmt.Println(" (IRun2Impl) M3()")
}type TRun struct {IRun1IRun2
}func (e TRun) M1() {fmt.Println("t m1")
}func main() {e := TRun{IRun1: &IRun1Impl{},IRun2: &IRun2Impl{},}e.M1()// 不在类型上声明M2方法,虽然两个接口都有声明M2方法,但是也会报错:// 编译器报错:Ambiguous reference 'M2'e.M2()e.M3()}

示例三:

package mainimport "fmt"type IRun1 interface {M1()M2()
}type IRun2 interface {M2()M3()
}type IRun1Impl struct{}func (IRun1Impl) M1() {fmt.Println(" (IRun1Impl) M1()")
}func (IRun1Impl) M2() {fmt.Println(" (IRun1Impl) M2()")
}type IRun2Impl struct{}func (IRun2Impl) M2() {fmt.Println(" (IRun2Impl) M2()")
}func (IRun2Impl) M3() {fmt.Println(" (IRun2Impl) M3()")
}type TRun struct {IRun1IRun2
}func (e TRun) M1() {fmt.Println("t m1")
}func main() {e := TRun{IRun1: &IRun1Impl{},IRun2: &IRun2Impl{},}e.M1()// 虽然没有在类型上声明M2方法,但是不调用M2方法的话也不会存在编译错误// 满足原则:你用你写,不用不写(u can u up)//e.M2()e.M3()
}

小提示:现实中应该避免这种复杂编程,显然无疑的提高了问题复杂度,并无显著收益。

类型里面内嵌接口

type InterfaceX interface {M1()M2()
}type TS struct {InterfaceX
}func (TS) M3() {}

类型TS内嵌接口InterfaceX是允许的,而且编译器不要求强制必须实现M1与M2方法,这个如果有Java经验的话会很违背经验。但是Golang就是允许的,但是如果你调用未实现的方法就会报错:

func main() {var t TSt.M1()
}

查看一下方法集合:

func main() {dump_method_set.DumpMethodSet((*InterfaceX)(nil))var t TSvar pt *TSdump_method_set.DumpMethodSet(&t)dump_method_set.DumpMethodSet(&pt)
}

输出:

类型中内嵌接口,命名冲突的方法调用优先级
package maintype Interface interface {M1()M2()
}type T struct {Interface
}// 类型T上实现了接口M1方法,但是类型T未实现M2方法
func (T) M1() {println("T's M1")
}type S struct{}func (S) M1() {println("S's M1")
}
func (S) M2() {println("S's M2")
}func main() {var t = T{Interface: S{},}// 因为类型实现了M1方法,所以直接调用M1的方法t.M1()// 因为接口类型没有实现M2方法,所以调用会从内嵌的接口上寻找方法t.M2()
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/1511.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis 如何实现分布式锁

课程地址 单机 Redis naive 版 加锁&#xff1a; SETNX ${lockName} ${value} # set if not exist如果不存在则插入成功&#xff0c;返回 1&#xff0c;加锁成功&#xff1b;否则返回 0&#xff0c;加锁失败 解锁&#xff1a; DEL ${lockName}问题1 2 个线程 A、B&#…

前后端交互概念

前后端交互概念 1前后端分离开发概念2搭建后端环境2.1配置文件commomcommon-utilservice-utilmodelservice gitee使用 1前后端分离开发概念 前段&#xff1a;运用html、css、js和现成库&#xff0c;对数据作展示。 后端&#xff1a;运用Java和Java框架&#xff0c;提供数据或操…

立创·实战派ESP32-C3开发板 with lv_micropython

一、lv_micropython对驱动芯片的支持 ESP32-C3开发板的Display drivers:ST7789&#xff0c;Input drivers:FT6336&#xff0c;从LVGL的官方文档了解到lv_micropython包含了这两颗IC的驱动。 参考文档&#xff1a; lv_micropython already contains these drivers: 链接:Micro…

智慧化转型赋能园区创新:科技创新引领产业智慧化,打造高效发展新格局

在全球化和信息化浪潮的推动下&#xff0c;园区作为区域经济发展的重要引擎&#xff0c;正面临着前所未有的机遇与挑战。为应对这些挑战并把握机遇&#xff0c;园区需积极拥抱智慧化转型&#xff0c;通过科技创新引领产业智慧化&#xff0c;打造高效发展的新格局。本文将深入探…

贝叶斯分类 python

贝叶斯分类 python 贝叶斯分类器是一种基于贝叶斯定理的分类方法&#xff0c;常用于文本分类、垃圾邮件过滤等领域。 在Python中&#xff0c;我们可以使用scikit-learn库来实现贝叶斯分类器。 下面是一个使用Gaussian Naive Bayes(高斯朴素贝叶斯)分类器的简单示例&#xff1…

go | defer、panic、recover

刷一道题&#xff0c; 将当函数触发panic 之后&#xff0c;函数是怎么执行的 然后我去找相关博客&#xff0c;发现这篇讲的蛮好的 接下来我直接上demo &#xff0c;然后通过demo 来逐个分析 package mainimport ("fmt" )func f() {defer func() {if r : recover();…

毕业设计——基于ESP32的智能家居系统(语音识别、APP控制)

ESP32嵌入式单片机实战项目 一、功能演示二、项目介绍1、功能演示2、外设介绍 三、资料获取 一、功能演示 多种控制方式 ① 语音控制 ②APP控制 ③本地按键控制 ESP32嵌入式单片机实战项目演示 二、项目介绍 1、功能演示 这一个基于esp32c3的智能家居控制系统&#xff0c;能实…

websocket 请求头报错 Provisional headers are shown 的解决方法

今日简单总结 websocket 使用过程中遇到的问题&#xff0c;主要从以下三个方面来分享&#xff1a; 1、前端部分 websocket 代码 2、使用 koa.js 实现后端 websocket 服务搭建 3、和后端 java Netty 库对接时遇到连接失败问题 一、前端部分 websocket 代码 <template>…

Spark和Hadoop的安装

实验内容和要求 1&#xff0e;安装Hadoop和Spark 进入Linux系统&#xff0c;完成Hadoop伪分布式模式的安装。完成Hadoop的安装以后&#xff0c;再安装Spark&#xff08;Local模式&#xff09;。 2&#xff0e;HDFS常用操作 使用hadoop用户名登录进入Linux系统&#xff0c;启动…

Flink基础概念及算子

Flink基础概念-算子 一、Flink概述二、Flink集群角色和核心概念1.Flink运行时架构&#xff08;Standealone会话模式&#xff09;2.并行度&#xff08;Parallelism&#xff09;3.算子链&#xff08;Operator Chain&#xff09;4. 任务槽&#xff08;Task Slots&#xff09; 三、…

GO环境及入门案例

文章目录 简介一、win GO开发环境安装二、Linux go运行环境二、GO代码入门2.1 导包案例2.2 赋值2.3 变量、函数2.4 三方库使用 简介 go不是面向对象语言&#xff0c; 其指针、结构体等比较像C&#xff0c;知名的go 开源项目有docker k8s prometheus node-exporter等 一、win …

C语言语法进阶

条件运算符 条件运算符是 C 语言中唯一的一种三目运算符。三目运算符代表有三个操作数&#xff1b;双目 运算符代表有两个操作数&#xff0c;如逻辑与运算符就是双目运算符&#xff1b;单目运算符代表有一个操作数&#xff0c; 如逻辑非运算符就是单目运算符。运算符也称操作符…

arping命令详解

arping – send ARP REQUEST to a neighbour host. arping 是一个在网络中发送 ARP 请求以查找特定 IP 地址对应的 MAC 地址的命令行工具。它的功能类似于 ping 命令&#xff0c;基于ARP协议报文的交互机制&#xff0c;只能测试同一网段或子网的网络主机的连通性。 ARP 是 Add…

软件杯 深度学习实现行人重识别 - python opencv yolo Reid

文章目录 0 前言1 课题背景2 效果展示3 行人检测4 行人重识别5 其他工具6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习的行人重识别算法研究与实现 ** 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c…

如何使用JSONB类型在PostgreSQL中存储和查询复杂的数据结构?

文章目录 解决方案1. 创建包含JSONB列的表2. 插入JSONB数据3. 查询JSONB数据4. 创建索引以优化查询性能 示例代码结论 在PostgreSQL中&#xff0c;JSONB是一种二进制格式的JSON数据类型&#xff0c;它允许你在数据库中存储和查询复杂的JSON数据结构。与普通的JSON类型相比&…

《操作系统导论》第27章读书笔记:插叙:线程API

《操作系统导论》第27章读书笔记&#xff1a;插叙&#xff1a;线程API —— 2024-04-21 杭州 上午 本章讲得比较啰嗦&#xff0c;问题是本章的二级标题后面都会作为一个章节来讲&#xff0c;所以本章属于概况介绍类章节&#xff0c;另外这几个并发的章节使用的都是是POSIX线程…

【python】启动一个公司级项目的完整报错和解决方案

启动一个项目对于新手都是不容易的事情 操作 打开项目 使用pyCharm打开python项目以后&#xff0c;先找main方法&#xff0c;一般在根目录有一个.py的文件 点进去以后会让你配置Python解释器 每个项目都有自己的一个虚拟环境&#xff0c;配置自己的解释器&#xff0c;可能…

windows驱动开发-设备栈

设备栈是windows内核中非常重要的部分&#xff0c;这部分理解可以让我们在调试中节省大量的时间&#xff0c; 在windows NT体系中&#xff0c;内核所有的设备被按照连接次序加载到设备树上&#xff0c;这棵树的根节点是ROOT节点&#xff0c;每一个设备可以从当前路径一直遍历到…

QMT和Ptrade有什么区别?该如何选择?

QMT&#xff08;Quantitative Model Trading&#xff09;和Ptrade&#xff08;Professional Trading&#xff09;是两种不同的交易策略和方法&#xff0c;它们在金融市场中被广泛应用。了解它们的区别有助于投资者根据自己的需求和目标做出选择&#xff1a; QMT&#xff08;量…

将记录从excel当中导出为.sql文件,再新增到数据库

一、背景 临时遇到了一个需求&#xff0c;比如根据人员的名字查询对应记录&#xff0c;看起来还是很简单的&#xff0c;直接用select查询就可以&#xff0c;然而如果此时存在以下情况&#xff1a; 数据库根本就没有人员信息表&#xff1b;------这个倒是好操作&#xff1b;现…