go语言学习--4.方法和接口

目录

1.方法

2.接口

2.1结构体类型

2.2具体类型向接口类型赋值

2.3获取接口类型数据的具体类型信息

3.channel

3.1阻塞式读写channel操作

2.3非阻塞式读写channel操作

4.map

4.1插入数据

4.2删除数据

4.3查找数据

4.4扩容


1.方法

方法一般是面向对象编程(OOP)的一个特性,在C++语言中方法对应一个类对象的成员函数,是关联到具体对象上的虚表中的。但是Go语言的方法却是关联到类型的,这样可以在编译阶段完成方法的静态绑定。一个面向对象的程序会用方法来表达其属性对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。

实现C语言中的一组函数如下:

// 文件对象
type File struct {fd int
}// 打开文件
func OpenFile(name string) (f *File, err error) {// ...
}// 关闭文件
func CloseFile(f *File) error {// ...
}// 读文件数据
func ReadFile(f *File, offset int64, data []byte) int {// ...
}

以上的三个函数都是普通的函数,需要占用包级空间中的名字资源。不过CloseFile和ReadFile函数只是针对File类型对象的操作,这时候我们更希望这类函数和操作对象的类型紧密绑定在一起。

所以在go语言中我们修改如下: 

// 关闭文件
func (f *File) CloseFile() error {// ...
}// 读文件数据
func (f *File) ReadFile(offset int64, data []byte) int {// ...
}

        将CloseFile和ReadFile函数的第一个参数移动到函数名的开头,这两个函数就成了File类型独有的方法了(而不是File对象方法)

        从代码角度看虽然只是一个小的改动,但是从编程哲学角度来看,Go语言已经是进入面向对象语言的行列了。我们可以给任何自定义类型添加一个或多个方法。每种类型对应的方法必须和类型的定义在同一个包中,因此是无法给int这类内置类型添加方法的(因为方法的定义和类型的定义不在一个包中)。对于给定的类型,每个方法的名字必须是唯一的,同时方法和函数一样也不支持重载。

2.接口

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

Go的接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让对象更加灵活和更具有适应能力。很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的鸭子类型。

所谓鸭子类型说的是:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。Go语言中的面向对象就是如此,如果一个对象只要看起来像是某种接口类型的实现,那么它就可以作为该接口类型使用。

就比如说在c语言中,使用printf在终端输出的时候只能输出有限类型的几个变量,而在go中可以使用fmt.Printf,实际上是fmt.Fprintf向任意自定义的输出流对象打印,甚至可以打印到网络甚至是压缩文件,同时打印的数据不限于语言内置的基础类型,任意隐士满足fmt.Stringer接口的对象都可以打印,不满足fmt.Stringer接口的依然可以通过反射的技术打印。

2.1结构体类型

interface实际上就是一个结构体,包含两个成员。其中一个成员是指向具体数据的指针,另一个成员中包含了类型信息。空接口和带方法的接口略有不同,下面分别是空接口的数据结构:

struct Eface
{Type*    type;void*    data;
};

其中的Type指的是:

struct Type
{uintptr size;uint32 hash;uint8 _unused;uint8 align;uint8 fieldAlign;uint8 kind;Alg *alg;void *gc;String *string;UncommonType *x;Type *ptrto;
};

和带方法的接口使用的数据结构:

struct Iface
{Itab*    tab;void*    data;
};

其中的Iface指的是:

struct    Itab
{InterfaceType*    inter;Type*    type;Itab*    link;int32    bad;int32    unused;void    (*fun[])(void);   // 方法表
};

2.2具体类型向接口类型赋值

将一个具体类型数据赋值给interface这样的抽象类型,需要进行类型转换。这个转换过程中涉及哪些操作呢?

如果转换为空接口,返回一个Eface,将Eface中的data指针指向原型数据,type指针会指向数据的Type结构体。

如果将其转化为带方法的interface,需要进行一次检测,该类型必须实现interface中声明的所有方法才可以进行转换,这个检测将会在编译过程中进行。检测过程具体实现式通过比较具体类型的方法表和接口类型的方法表来进行的。

  • 具体类型方法表:Type的UncommonType中有一个方法表,某个具体类型实现的所有方法都会被收集到这张表中。
  • 接口类型方法表:Iface的Itab的InterfaceType中也有一张方法表,这张方法表中是接口所声明的方法。Iface中的Itab的func域也是一张方法表,这张表中的每一项就是一个函数指针,也就是只有实现没有声明。

这两处方法表都是排序过的,只需要一遍顺序扫描进行比较,应该可以知道Type中否实现了接口中声明的所有方法。最后还会将Type方法表中的函数指针,拷贝到Itab的fun字段中。Iface中的Itab的func域也是一张方法表,这张表中的每一项就是一个函数指针,也就是只有实现没有声明。

2.3获取接口类型数据的具体类型信息


接口类型转换为具体类型(也就是反射,reflect),也涉及到了类型转换。reflect包中的TypeOf和ValueOf函数来得到接口变量的Type和Value。

3.channel

go中的channel是可以被存储在变量中,可以作为参数传递给函数,也可以作为函数返回值返回,我们先来看一下channel的结构体定义:

struct    Hchan
{uintgo    qcount;            // 队列q中的总数据数量uintgo    dataqsize;        // 环形队列q的数据大小uint16    elemsize;			// 当前队列的使用量bool    closed;				uint8    elemalign;Alg*    elemalg;        // interface for element typeuintgo    sendx;            // 发送indexuintgo    recvx;            // 接收indexWaitQ    recvq;            // 因recv而阻塞的等待队列WaitQ    sendq;            // 因send而阻塞的等待队列Lock;
};

Hchan结构体中的核心部分是存放channel数据的环形队列,相关数据的作用已经在其后做出了备注。在该结构体中没有存放数据的域,如果是带缓冲区的chan,则缓冲区数据实际上是紧接着Hchan结构体中分配的。

另一个重要部分就是recvq和sendq两个链表,一个是因读这个通道而导致阻塞的goroutine,另一个是因为写这个通道而阻塞的goroutine。如果一个goroutine阻塞于channel了,那么它就被挂在recvq或sendq中。WaitQ是链表的定义,包含一个头结点和一个尾结点,该链表中中存放的成员是一个sudoG结构体变量,具体定义如下:

struct    SudoG
{G*    g;        // g and selgen constituteuint32    selgen;        // a weak pointer to gSudoG*    link;int64    releasetime;byte*    elem;        // data element
};

该结构体中最主要的是g和elem。elem用于存储goroutine的数据。读通道时,数据会从Hchan的队列中拷贝到SudoG的elem域。写通道时,数据则是由SudoG的elem域拷贝到Hchan的队列中。

Hchan结构如下:

3.1阻塞式读写channel操作

写操作代码如下,其中的c就是channel,v指的是数据:

c <- v

事实上基本的阻塞模式写channel操作在底层运行时库中对应的是一个runtime.chansend函数。具体如下:

void runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)

其中的ep指的是变量v的地址,这里的传值约定是调用者负责分配好ep的空间,仅需要简单的取变量地址就好了,pres是在select中的通道操作中使用的。

阻塞模式读操作的核心函数有两种包装如下:

chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)

以及

chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected)

这两种的区别主要在于返回值是否会返回一个bool类型值,该值只是用于判断channel是否能读取出数据。

读写操作的以上阻塞的过程类似,故而不再做出说明,我们补充三个细节:

  • 以上我们都强调是阻塞式的读写操作,其实相对应的也有非阻塞的读写操作,使用过select-case来进行调用的。
  • 空通道,指的是将一个channel赋值为nil,或者调用后不适用make进行初始化。读写空通道是永远阻塞的。
  • 关闭的通道,永远不会阻塞,会返回一个通道数据类型的零值。首先将closed置为1,第二步收集读等待队列recvq的所有sg,每个sg的elem都设为类型零值,第三步收集写等待队列sendq的所有sg,每个sg的elem都设为nil,最后唤醒所有收集的sg。

2.3非阻塞式读写channel操作


如上文所说,非阻塞式其实就是使用select-case来实现,在编译时将会被编译为if-else。

如:

select {
case v = <-c:...foo
default:...bar
}

就会被编译为:

if selectnbrecv(&v, c) {...foo
} else {...bar
}


至于其中的selectnbrecv相关的函数简单地调runtime.chanrecv函数,设置了一个参数,告诉runtime.chanrecv函数,当不能完成操作时不要阻塞,而是返回失败。

但是select中的case的执行顺序是随机的,而不像switch中的case那样一条一条的顺序执行。让每一个select都对应一个Select结构体。在Select数据结构中有个Scase数组,记录下了每一个case,而Scase中包含了Hchan。然后pollorder数组将元素随机排列,这样就可以将Scase乱序了。

4.map

map表的底层原理是哈希表,其结构体定义如下:

type Map struct {Key  *Type // Key typeElem *Type // Val (elem) typeBucket *Type // 哈希桶Hmap   *Type // 底层使用的哈希表元信息Hiter  *Type // 用于遍历哈希表的迭代器
}

其中的Hmap 的具体化数据结构如下:

type hmap struct {// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.// Make sure this stays in sync with the compiler's definition.count     int // map目前的元素数目flags     uint8 // map状态(正在被遍历/正在被写入)B         uint8  // 哈希桶数目以2为底的对数(哈希桶的数目都是 2 的整数次幂,用位运算来计算取余运算的值, 即 N mod M = N & (M-1)))noverflow uint16 //溢出桶的数目, 这个数值不是恒定精确的, 当其 B>=16 时为近似值hash0     uint32 // 随机哈希种子buckets    unsafe.Pointer // 指向当前哈希桶的指针oldbuckets unsafe.Pointer // 扩容时指向旧桶的指针nevacuate  uintptr        // 桶进行调整时指示的搬迁进度extra *mapextra // 表征溢出桶的变量
}

以上hmap基本都是涉及到了哈希桶和溢出桶,我们首先看一下它的数据结构,如下:

type bmap struct {topbits  [8]uint8    // 键哈希值的高8位keys     [8]keytype  // 哈希桶中所有键elems    [8]elemtype	// 哈希桶中所有值//pad      uintptr(新的 go 版本已经移除了该字段, 我未具体了解此处的 change detail, 之前设置该字段是为了在 nacl/amd64p32 上的内存对齐)overflow uintptr
}

我们会发现哈希桶bmap一般指定其能保存8个键值对,如果多于8个键值对,就会申请新的buckets,并将其于之前的buckets链接在一起。

其中的联系如图所示:

在具体插入时,首先会根据key值采用相应的hash算法计算对应的哈希值,将哈希值的低8位作为Hmap结构体中buckets数组的索引,找到key值所对应的bucket,将哈希值的高8位催出在bucket的tophash中。

特点如下:

  1. map是无序的(原因为无序写入以及扩容导致的元素顺序发生变化),每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取
  2. map的长度是不固定的,也就是和slice一样,也是一种引用类型内置的len函数同样适用于map,返回map拥有的key的数量
  3. map的key可以是所有可比较的类型,如布尔型、整数型、浮点型、复杂型、字符串型……也可以键。

 如下方式即可进行初始化:

var a map[keytype]valuetype
类型名意义
amap表名字
keytype键类型
valuetype键对应的值的类型

除此以外还可以使用make进行初始化,代码如下: 

map_variable = make(map[key_data_type]value_data_type)

我们还可以使用初始值进行初始化,如下:

var m map[string]int = map[string]int{"hunter":12,"tony":10}

4.1插入数据

map的数据插入代码如下:

map_variable["mars"] = 27

插入过程如下:

  1. 根据key值计算出哈希值
  2. 取哈希值低位和hmap.B取模确定bucket位置
  3. 查找该key是否已经存在,如果存在则直接更新值
  4. 如果没有找到key,则将这一对key-value插入

4.2删除数据

delete(map, key) 函数用于删除集合的元素, 参数为 map 和其对应的 key。删除函数不返回任何值。相关代码如下:

   countryCapitalMap := map[string] string {"France":"Paris","Italy":"Rome","Japan":"Tokyo","India":"New Delhi"}/* 删除元素 */delete(countryCapitalMap,"France");

4.3查找数据

通过key获取map中对应的value值。语法为:map[key] .

但是当key如果不存在的时候,我们会得到该value值类型的默认值,比如string类型得到空字符串,int类型得到0。但是程序不会报错。

所以我们可以使用ok-idiom获取值,如下:value, ok := map[key] ,其中的value是返回值,ok是一个bool值,可知道key/value是否存在。

在map表中的查找过程如下:

  1. 查找或者操作map时,首先key经过hash函数生成hash值
  2. 通过哈希值的低8位来判断当前数据属于哪个桶
  3. 找到桶之后,通过哈希值的高八位与bucket存储的高位哈希值循环比对
  4. 如果相同就比较刚才找到的底层数组的key值,如果key相同,取出value
  5. 如果高八位hash值在此bucket没有,或者有,但是key不相同,就去链表中下一个溢出bucket中查找,直到查找到链表的末尾
  6. 如果查找不到,也不会返回空值,而是返回相应类型的0值。

4.4扩容

哈希表就是以空间换时间,访问速度是直接跟填充因子相关的,所以当哈希表太满之后就需要进行扩容。

如果扩容前的哈希表大小为2B扩容之后的大小为2(B+1),每次扩容都变为原来大小的两倍,哈希表大小始终为2的指数倍,则有(hash mod 2B)等价于(hash & (2B-1))。这样可以简化运算,避免了取余操作。

  • 1.触发扩容的条件?

负载因子(负载因子 = 键数量/bucket数量) > 6.5时,也即平均每个bucket存储的键值对达到6.5个。
溢出桶(overflow)数量 > 2^15时,也即overflow数量超过32768时。

  • 什么是增量扩容呢?

        如果负载因子>6.5时,进行增量扩容。这时会新建一个桶(bucket),新的bucket长度是原来的2倍,然后旧桶数据搬迁到新桶。每个旧桶的键值对都会分流到两个新桶中

主要是缩短map容器的响应时间。

        假如我们直接将map用作某个响应实时性要求非常高的web应用存储,如果不采用增量扩容,当map里面存储的元素很多之后,扩容时系统就会卡往,导致较长一段时间内无法响应请求。不过增量扩容本质上还是将总的扩容时间分摊到了每一次哈希操作上面。

  • 什么是等量扩容?它的触发条件是什么?进行等量扩容后的优势是什么?

等量扩容,就是创建和旧桶数目一样多的新桶,然后把原来的键值对迁移到新桶中,重新做一遍类似增量扩容的搬迁动作。

  • 触发条件:负载因子没超标,溢出桶较多。这个较多的评判标准为:

如果常规桶数目不大于2^15,那么使用的溢出桶数目超过常规桶就算是多了;
如果常规桶数目大于215,那么使用溢出桶数目一旦超过215就算多了。
这样做的目的是把松散的键值对重新排列一次,能够存储的更加紧凑,进而减少溢出桶的使用,以使bucket的使用率更高,进而保证更快的存取。

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

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

相关文章

《由浅入深学习SAP财务》:第2章 总账模块 - 2.6 定期处理 - 2.6.2 月末操作:GR/IR重组

2.6.2 月末操作&#xff1a;GR/IR重组 SAP在采购订单收货和发票校验时分别产生凭证&#xff0c;中间采用GR/IR过渡。GR即为收货&#xff0c;IR即为收票。月末&#xff0c;GR/IR的余额根据收货和收票的情况进行判断&#xff0c;转入“应付暂估”或“在途物资”&#xff0c;次月自…

C++ 数据类型

数据类型介绍 数据类型的作用&#xff1a;编译器预算数据分配的内存空间大小。 ps&#xff1a;可以通俗理解为&#xff1a;数据类型是用来规范内存的开销&#xff0c;约定数据在内存中的格式&#xff0c;便于存储。 变量 变量的语法 在计算机程序中&#xff0c;变量是用来存…

安装苹果ipa的方法

1、如何生成udid udid获取工具 https://www.betaqr.com/tools 提示下载后 2、爱思助手安装苹果app mac下载爱思助手&#xff0c;数据线连接手机&#xff0c;把ipa包拖到爱思助手app应用选项里 3、新手机调试需要先选中设备&#xff0c;再注册

VMwear桥接网络正确配置+静态IP设置

1.桥接网络配置 很多时候在VMware安装完虚拟机之后&#xff0c;会发现配置的桥接网络没有起作用&#xff0c;如果是Linux下输入ifconfig发现只有ipv6的地址而没有ipv4&#xff0c;说明没有桥接没有启用成功&#xff0c;需要按照以下方式来设置 在VMware的左上角打开编辑&#…

Redis(持久化 -- RDB AOF)

持久化 通常我们认为持久化为: 重启进程/重启主机之后, 数据仍然存在不丢失 把数据存储在硬盘上 – 持久 把数据存储在内存中 – 不持久 Redis 持久化 redis 是一个内存数据库, 也就是说本身是不持久的(但是快[效率高]), 于是 Redis 提供了持久化机制 — RDB 和 AOF 二者都是对…

H.265网页无插件播放EasyPlayer.js流媒体播放器常见问题及解答

EasyPlayer属于一款高效、精炼、稳定且免费的流媒体播放器&#xff0c;可支持多种流媒体协议播放&#xff0c;无须安装任何插件&#xff0c;起播快、延迟低、兼容性强&#xff0c;使用非常便捷。 今天我们来汇总下用户常见的几个问题及解答。 1、EasyPlayer.js播放多路H.265视…

Android 输入法框架

输入法属于输入系统的一部分&#xff0c;区别于输入系统只能向系统产生时间&#xff0c;输入法能向系统输入具体的内容&#xff0c;下面来认识输入法的大体框架&#xff0c;以下内容参考清华大学出版社出版的《Android图形显示系统》。 输入法框架包含3个组件&#xff0c;各组件…

python画图Matplotlib和Seaborn

python画图Matplotlib和Season 一、Matplotlib1、介绍2、安装3、内容二、Seaborn1、介绍2、安装3、内容一、Matplotlib Matplotlib官网 1、介绍 Matplotlib 是一个 Python 的绘图库,用于创建高质量的二维图表和一些基本的三维图表。它广泛应用于科学计算、数据分析、工程学和…

基于springboot+vue实现的的成人教育教务系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;spring…

AI时代,搜索引擎的巨头地位恐怕不保了

兄弟们&#xff0c;你们使用搜索网站的频率有降低吗&#xff1f; ChatGPT 已经流行了一年多了&#xff0c;这期间数个大模型都发展了起来。 搜索引擎本质上也属于问答系统&#xff0c;所以&#xff0c;在大模型成熟之后&#xff0c;我使用搜索的频率越来越低了。 主要是因为…

水牛社:互联网赚钱秘籍,免费项目,你真敢要吗?

免费是最贵的。真正理解并使用这句话的只有少数人&#xff0c;今天在网上分享一下免费项目背后的逻辑&#xff0c;抛开现象&#xff0c; 本质是最重要的。 我从事互联网工作15年。不管是过去还是现在&#xff0c;总有人喜欢问有没有免费项目&#xff1f; 其实我平时懒得回答…

java基础语法(13)

1. final关键字 final概述 学习了继承后&#xff0c;我们知道&#xff0c;子类可以在父类的基础上改写父类内容&#xff0c;比如&#xff0c;方法重写。那么我们能不能随意的继承API中提供的类&#xff0c;改写其内容呢&#xff1f;显然这是不合适的。为了避免这种随意改写的情…

C++初阶---vector(STL)

1、vector的介绍和使用 1.1、vector的介绍 1. vector是表示可变大小数组的序列容器。 2. 就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素 进行访问&#xff0c;和数组一样高效。但是又不像数组&#xff0c;它的大小是…

MATLAB 普通场景的道路点云分割 (方法一)(56)

MATLAB 普通场景的道路点云分割(方法一) (56) 一、分割原理二、算法实现1.代码一、分割原理 基于这样一个认识:大部分情况下,点云都是分块去处理的,在某块点云场景中,点云区域不大,地面基本是水平分布的,不会有较大的坡度,因此将其认为是一个法向与Z轴大致平行的平…

Python空间分析简明教程

数据世界是一个活生生的、会呼吸的事物。 当一个城市的犯罪率上升时&#xff0c;这是因为现实世界中有人在某个地方犯罪。 有警察局、住宅区和商业区、人口密度以及可以与位置相关联的人的地方。 所有这些东西都存在于数据框和表格之外的世界中。 空间分析使数据科学家能够回答…

实战环境-Activiti7从入门到专家(4)

背景 对于activiti7 已经有了感性认知&#xff0c;并且已经获得了源代码&#xff0c;梳理了核心的API。后面还有大量的内容&#xff0c;包括BPMN规范的落地&#xff0c;但是我们不能只停留在理论层次&#xff0c;需要从实际罗德的内容展开&#xff0c;因此需要构建实战环境。 …

WD西部数据正式通知客户:HDD与NAND继续涨价!

人工智能&#xff08;AI&#xff09;市场的快速增长引发了对数据存储的巨大需求。加之去年因市场环境因素导致HDD制造商减产&#xff0c;自去年下半年以来&#xff0c;高容量HDD供不应求&#xff0c;致使整体HDD价格显著上涨。据TechNews援引的行业消息指出&#xff0c;自去年第…

[每日算法 - 阿里机试] leetcode739. 每日温度

入口 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能&#xff0c;轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/daily-temperatures/descr…

windows安装charles抓包iphone

安装charles抓包iphone charles基础介绍windows安装 charles基础介绍 Charles 是在 PC 端常用的网络封包截取工具&#xff0c;在做移动开发时&#xff0c;我们为了调试与服务器端的网络通讯协议&#xff0c;常常需要截取网络封包来分析。除了在做移动开发中调试端口外&#xf…

不允许在constexpr函数中进行声明

这是我用pycharm在windows系统下复现sfm深度学习网络(Deep Two-View Structure-from-Motion Revisited&#xff09;遇见的问题&#xff0c;复现时有段代码pytorch扩展cuda/c&#xff0c;pycharm中出现C标准相关的报错如下&#xff1a; 在网上查找很久无果&#xff0c;后面通过…