Go 语言相关学习计划 暂定自学路线
Go 源码 Go runtime Go mem Go map, chan, slice and so on source code Go epoll 其他项目源码 etcd docker ethereum 基础 操作系统 linux 数据结构 算法 架构 如何设计缓存系统
通用区块链浏览器引擎泛想 1.背景 目前区块链行业各种链横行,公链有 ethereum、bitcoin、filecoin 等,联盟链有 fabric、fisco bcos、XuperChain、ChainMaker 等,而且每个链的浏览器都需要单独开发,因为每个链的区块、交易、账户体系、链上接口都是不一样的,因此每个链都要有适用自己的浏览器,但是大多数浏览器的功能还是比较相同的,大多数都是存储区块、交易、账户、合约、组织、节点等信息,但是对于公链和联盟路的区别比较大,比如公链大多数都有原生代币以及允许发行代币,联盟链甚至没有代币,并且还有节点准入、业务隔离等特点。但从功能上来说,区块链离不开区块、交易、账户、合约这些东西,因此开发一款通用的区块链浏览器引擎还是很有必要的。
2.什么是区块链浏览器引擎 区块链浏览器的功能很简单,都是将链上的数据存储到数据库,同时提供数据查询的接口,比如查询区块信息,合约信息,账户交易等。那么这部分功能可以做成通用的。通用区块链浏览器引擎最难的一点是如何适配所有的区块链。
不从实现角度说,只从功能来说,通用区块链浏览器引擎就是开发者可以在此基础上非常简单的开发出自己区块链浏览器,也就是说,开发者只需要做很少量、很简单的开发就可以适配一个区块链。
3.难点 先说如何针对一条链开发对应的浏览器。首先要知道链的数据结构,包括区块、交易、账户体系、合约相关数据结构、代币、组织等等。然后确定浏览器的功能。之后选择爬取链上数据的方式,比如按照区块高度爬取、事件订阅方式等等。之后就可以编码了。简而言之,就是确定区块链数据结构、确定浏览器功能、确定链上接口、确定浏览器程序架构(接口、数据库等)。
对于上面的四点,其中确定浏览器程序结构算是最简单的,只有数据库的选择会有一点问题,因为大多数数据库都能满足需求。
最难得是区块链数据结构与链上接口以及浏览器的功能。也就是在开发者使用区块链浏览器引擎时,如何最简单的告诉引擎区块链的数据结构和链上的接口,以及想要的功能。举一个例子,在联盟链中,fabric 有 channel 和 组织,XuperChain 平行链和群组,fisco bocos 有群组和 channel,ChainMaker 有平行链和组织,虽然功能类似,但是引擎如何适配这些概念是一个难点。另外我们不知道接下来还会出现什么样的奇怪的概念,那么引擎该如何做呢?这是通用区块链浏览器引擎的最大难点!
3.1 接口类型 目前大多数区块链对外的接口有两种:json-rpc 和 grpc,json-rpc 是可以使用 http 或者 ws 的 json 格式数据 rpc,grpc 底层使用的是 http2,可以通过这些接口查询链上信息。如何统一呢?
3.2 数据结构 对于链上的区块、交易、合约等数据,都是 go 中的 struct。
第一每个链的结构不一样,第二每个链上的概念不统一,而且后续可能有很多新的概念。
这个问题如何解决呢?
4.解决方案(第一版) 4.1接口解决方案 对于各种类型的接口,没必要全部实现,也就是不需要在引擎中实现 json-rpc、grpc、https 等所有接口,只需要在引擎中定义一个接口,使用引擎的开发者实现这个接口,也就是把客户端与服务端的连接实现这个接口,比如引擎有接口 ClientInterface
type ClientInterface interface { QueryBlockByHeight() QUeryTxByID() ...... } 开发者将真正的与链上的连接封装一下,同时实现 ClientInterface 接口,那么引擎可以使用这个对象去做链上的数据查询。
4.2数据结构解决方案 对于数据结构同样采用接口的方式,区块结构举例,定义 Blocker 接口
type Blocker interface { Bytes() Load() .
“ 最近在使用 Go 语言写一个区块链的 SDK,从设计到上线总结了其中的一些问题以及思考,如果你也在写 SDK 或者要写 SDK,希望这篇文章可以帮到你。这篇文件不会区分是否是区块链的 SDK,下一篇文章主要写区块链的 SDK 与普通系统 SDK 的一些区别以及相关思考”
当写一个 SDK 时,第一需要考虑的问题应该是这个 SDK 的功能,或者说这个 SDK 是给什么样的用户来用,或者用户使用这个 SDK 能做什么。弄明白了这个问题后才可以开始动手。
一个系统的接口肯定是知道的,但有些情况并不是一个接口就能够完成一个用户的操作或者需求,此时需要系统的多个接口组合调用才可以,同时在调用多个接口时,有很多逻辑是需要在客户端完成的,那么接口调用以及用户的逻辑就需要在 SDK 中完成。所以简单来说,一个 SDK 的基础功能就是系统的接口调用与用户的部分逻辑(当然这只是我抽象出来的,实际情况 SDK 有很多种)。
我个人把 SDK 的出生过程总结如下:
SDK 功能确认; 系统接口逻辑确认; SDK 接口设计; SDK 架构设计; SDK 使用文档或者示例代码; 测试; 接下来就分这七步来聊一聊如何开发一个易用、可靠的 SDK。
1. SDK 功能确认 某个系统的 SDK 会有官方的 SDK 也有其他人贡献的 SDK,比如有的企业或者开发者觉得官方提供的 SDK 功能不全,或者使用不方便,满足不了现在的需求.
而且官方很大几率不会再开发一个专门的 SDK 来满足个别的需求,所以开发者有可能根据系统的接口来开发或者在官方 SDK 基础上添加功能。
对于这种现象的出现,我觉得主要原因在于官方的 SDK 在设计之初就是存在问题的,或者说 SDK 的功能确认是错误。
系统可能有几十个接口,但是对于用户有用的或者最常用的可能只有一半或者三分之一,所以 SDK 并不需要把所有的接口都支持,而是要把核心的功能筛选出来,这一点并不与上面提到的功能确认存在问题。
1. 简介 很多系统中都有守护进程,Go 中也不例外。Go 的系统监控主要目的就是隔一段时间来检查一下运行时是否进入异常状态。本文会介绍一下 sysmon 的创建以及其在系统中的作用。
关于本文整理了一个大体的概括图(建议保存此图片,以后复习就就不需要再通篇读文章了):
2. sysmon 的创建 对于其创建非常简单,在 runtime.main 函数(位于:go/src/runtime/proc.go 文件中)中启动程序时创建。基本流程如下:
调用 runtime.newm 函数,创建一个 runtime.m 结构对象。 runtime.newm 函数再调用 runtime.newm1 函数,然后调用 runtime.newsproc 函数,系统调用 clone 一个线程。 此时在创建好的 runtime.m 对象中已经存在了 sysmon 函数,在创建的线程中执行此函数,开始系统监控。 第一步检查是否有死锁。 然后开始无限循环。 循环最开始休眠时间为20us。 大约1ms后每次休眠时间翻倍(50个循环中都没有唤醒 goroutine,休眠时间翻倍)。 最长每次休眠时间10ms。 上面就是一个整体流程,本文没有将所有源码都复制过来,大家可以参考源码来理解上面这一过程。
3. sysmon 的作用 系统监控的左右主要如下:
debug 监控程序 强制垃圾回收 本质就是定期检查上一次垃圾回收是什么时间,如果长时间未进行垃圾回收,就强制执行一次。 网络轮询 10ms进行网络轮询 检查是否有待执行的 fd 将就绪的 goroutine 加入全局队列 如果存在空闲P,就用这个P来执行任务 抢占 可抢占系统调用的 P: 如果你不了解 Go 的 runtime 或者 MPG 模型,那么这部分理解可能困难一些。 我们知道一个 G 是运行的 P 上的,那么当这个 G 进行系统调用时,当前的 P 会怎样呢?是的,这时 P 其实就在等当前的 G,那么假如对于 P 的本地 G 队列还有在等待的 G时,对于这种情况我们肯定是不允许的。那么我们就需要对这个 P 进行抢占了。 在任务量过多时,所有的处理器(P)都在认真工作,此时对于系统调用的 P 就需要抢占了,目的就是大家都这么拼命,你就不要等你的那个系统调用的 G 了。 除了以上两种情况,假如这次系统调用时间过长,那么也会发起抢占的。 可抢占运行时间过长的 P: 当一个 G 在 P 上运行了很长时间,说明这个 G 开始欺负其他的 G 了,此时就需要对这个运行时间过长的 G 做出惩罚,抢占其 P 并将这个 G 放入全局队列。 此时关于 sysmon 的东西就讲完了,大家可以边看源码边理解此文章,但是建议对 Go 的 runtime 有一些了解之后再阅读抢占部分的内容。
1 哈希表 哈希表属于编程中比较常见的数据结构之一,基本上所有的语言都会实现数组和哈希表这两种结构,Hash table 的历史是比较悠远的,我们在编程时也是离不开的,这种数据结构的作用其实很简单,就是我们可以根据一个 key 可以查找到对应的 value,也就是说这种数据结构存储的是键值对的“列表”。
1.1 原理 首先哈希表中第一个点就是哈希函数,也就是我们需要一个函数,根据我们的 key 计算出一个值,然后根据这个值可以直接找到对应的 value。因为我们的哈希表的一个作用就是 O(1) 复杂度找到 key 对应的 value。
完美的哈希函数是可以做到将任何一个 key 值都可以计算出一个唯一且固定大小的值,不幸的是目前世界上还没有这种完美的哈希函数。因此我们需要解决的另外一个问题就是哈希冲突的解决。
1.1.1 哈希冲突 假如我们有两个不同的 key,通过哈希函数计算出的结果相同,那么我们是不能认为这两个 key 在 map 中是相同的,也就是如果出现了这种情况,我们的 map 结构是可以解决这个问题的。目前解决办法有很多,这里只说三个比较常见的解决方案:
开放地址法(Open Addressing):
写入时:假如 key Alice 与 Bob 通过哈希函数计算出结果冲突。当 map 中已经存在 key Alice,再写入 key 为 Bob时,发现哈希结果对应位置已经存在 Alice,此时在 Alice 位置之后再寻找位置,一直找到为空的位置,将 Bob 写入。 读取时:此时 map 中已存在 key Alice、Bob,且哈希结果相同,此时想查找 Bob 对应 value 时,先计算 Bob 哈希结果,再通过哈希结果在 map 中查找位置,此时由于和 Alice 哈希结果相同,并且 Alice 先于 Bob 存入 map,所以会直接找到 Alice 的位置,发现 key 是 Alice 不是 Bob,接着在 Alice 位置后面查找,直到找到 key Bob 或者找到空。 再哈希法(Re-Hashing):
1. chan
1.回顾切片 上一篇文章我们从源码的角度分析了切片,包括切片的数据结构,底层实现,扩容以及添加等,但是我们并没有详细分析切片扩容的规则到底是什么?尽管上一篇文章中展示了一些代码,可是为什么扩容结果是这样呢?今天我们就来详细的分析一波。
1.1. 示例代码 再回顾一下上一篇文章中关于扩容的代码:
s := []string{"a", "b"} // 此时切片长度为2,容量也为2。 s = append(s, "c") s = append(s, "d") s = append(s, "e") fmt.Printf("len=%d, cap=%d", len(s), cap(s)) // 结果:len=5, cap=8 s := []string{"a", "b"} // 此时切片长度为2,容量也为2。 s = append(s, "c", "d", "e") fmt.Printf("len=%d, cap=%d", len(s), cap(s)) // 结果:len=5, cap=5 s := []int{1, 2} // 此时切片长度为2,容量也为2。 s = append(s, 3, 4, 5) fmt.Printf("len=%d, cap=%d", len(s), cap(s)) // 结果:len=5, cap=6 2.
1. string 简介 string 肯定不陌生了,这个可以说是我们平时最常用的,其实字符串就是内存中一片连续的空间,我们也可以理解成字符的数组。在 Go 中我们是可以将 []byte 与 string 直接转换的,今天我们就仔细的聊一下 Go 语言中的 string 类型的一些东西。
2. 源码 2.1. string 类型源码 在 go/src/reflect/value.go 文件中,对于 string 类型有两个结构体,如下:
// StringHeader is the runtime representation of a string. // 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.
1.切片是啥 Go 的 **Slice(切片)**类型提供了一种方便有效的方法来处理类型化数据序列。 slice 与其他语言的数组类似却又不同,简单来说,切片更加灵活,用起来更方便,其原因就是可以扩容。
2.举例分析 2.1. 认识切片第一步 package main import "fmt" func main() { sliceExample() } func sliceExample() { slc := make([]int, 0) slc = append(slc, 1) fmt.Println(slc) var slc1 []int slc1 = append(slc1, 1) fmt.Println(slc1) var slc2 []int slc2 = append(slc2, 1) fmt.Println(slc2) } 上面的代码是最基本的使用方式,首先看一下上面的代码在底层都做了哪些东西(不要害怕汇编,其实很简单,我们主要明白部分汇编代码即可)
命令:go tool compile -S slice.go 我的 go 文件为 slice.go
"".main STEXT size=48 args=0x0 locals=0x8 0x0000 00000 (slice.go:5) TEXT "".main(SB), ABIInternal, $8-0 .