golang重写区块链——0.3 数据库存储链和实现命令行交互 - Go语言中文社区

golang重写区块链——0.3 数据库存储链和实现命令行交互


    到为止我们已经实现了一条能够进行pow的区块链,距离实现一条正真意义上的区块链还有很长的路需要走。我们可能已经发现每当我们关闭程序后重新只能重新创建新的链,之前的数据都会不存在了。原因就是我们目前的链存储的数据都是在内存中,并没有被写入电脑的磁盘,所以会随着程序的退出被擦除。这一节我们就要解决这个问题,使用数据库来永久地保存我们创建的链。

    比特币使用的是一款简约而不简单的数据库LevelDB,这次我们不采用此数据库,而我们选择使用既简单又简约的BlotDB数据库。关于这款数据库的详细信息大家可以在网上去详细的了解,这里就不做详细的说明。简单的提一点的就是这款数据库中存储数据的方式是类似golang语言中的map结构,是使用键值对映射的方式来存储数据的,这些键值对被存储在一个叫bucket(桶)中。我们实例一个数据库就是实例一种bucket。比如如果需要查找一个值,我们首先要进入到装此值的桶是哪个,然后通过此值对应的key来查找。

    在进行数据库存储之前,我们需要了解我们存储到数据库的有哪些信息。我们需要用到两个bucket(桶)来装我们的链信息。第一个bucket是存放blocks:一条链中包含的所有块中的元数据信息,另一个bucket是存放chainstate:存储链的状态信息。目前我们还没有涉及到交易,所以我们现在只考虑blocks bucket。

最终我们会用到的键值对为:

  1. 32 字节的 block-hash -> block 结构
  2. l -> 链中最后一个块的 hash

    因为在数据库中存储的数据的变成[]byte,所以我们需要序列化和反序列化的操作,我们在block包里面实现这两个函数;

//0.3 实现Block的序列化
func (b *Block) Serialize() []byte {
	//首先定义一个buffer存储序列化后的数据
	var result bytes.Buffer
	//实例化一个序列化实例,结果保存到result中
	encoder := gob.NewEncoder(&result)
	//对区块进行实例化
	err := encoder.Encode(b)
	if err != nil {
		log.Panic(err)
	}
	return result.Bytes()
}

//0.3 实现反序列化函数
func DeserializeBlock(d []byte) *Block {
	var block Block
	decoder := gob.NewDecoder(bytes.NewReader(d))
	err := decoder.Decode(&block)
	if err != nil {
		log.Panic(err)
	}
	return &block
}

    下面就要实现数据库的内容了:

让我们从 NewBlockchain 函数开始。在之前的实现中,它会创建一个新的
Blockchain 实例,并向其中加入创世块。而现在,我们希望它做的事情有:

  1. 打开一个数据库文件
  2. 检查文件里面是否已经存储了一个区块链
  3. 如果已经存储了一个区块链:
    1. 创建一个新的 Blockchain 实例
    2. 设置 Blockchain 实例的 tip 为数据库中存储的最后一个块的哈希
  4. 如果没有区块链:
    1. 创建创世块
    2. 存储到数据库
    3. 将创世块哈希保存为最后一个块的哈希
    4. 创建一个新的 Blockchain 实例,其 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希)

代码大概是这样:

//实例化一个区块链,默认存储了创世区块
func NewBlockchain() *Blockchain {
	//return &Blockchain{[]*block.Block{GenesisBlock()}}
	var tip []byte
	//打开一个数据库文件,如果文件不存在则创建该名字的文件
	db,err := bolt.Open(dbFile,0600,nil)
	if err != nil {
		log.Panic(err)
	}
	//读写操作数据库
	err = db.Update(func(tx *bolt.Tx) error{
		b := tx.Bucket([]byte(blocksBucket))
		//查看名字为blocksBucket的Bucket是否存在
		if b == nil {
			//不存在则从头 创建
			genesis := GenesisBlock()	//创建创世区块
			b,err := tx.CreateBucket([]byte(blocksBucket)) //创建名为blocksBucket的桶
			if err != nil {
				log.Panic(err)
			}
			err = b.Put(genesis.Hash,genesis.Serialize()) //写入键值对,区块哈希对应序列化后的区块
			if err != nil {
				log.Panic(err)
			}
			err = b.Put([]byte("l"),genesis.Hash) //"l"键对应区块链顶端区块的哈希
			if err != nil {
				log.Panic(err)
			}
			tip = genesis.Hash //指向最后一个区块,这里也就是创世区块
		} else {
			//如果存在blocksBucket桶,也就是存在区块链
			//通过键"l"映射出顶端区块的Hash值
			tip = b.Get([]byte("l"))
		}

		return nil
	})

	bc := Blockchain{tip,db}  //此时Blockchain结构体字段已经变成这样了
	return &bc

}

    接下来我们想要更新的是 AddBlock 方法:现在向链中加入区块,就不是像之前向一个数组中加入一个元素那么简单了。从现在开始,我们会将区块存储在数据库里面:

//把区块添加进区块链
func (bc *Blockchain) AddBlock(data string) {
	var lastHash []byte
	//只读的方式浏览数据库,获取当前区块链顶端区块的哈希,为加入下一区块做准备
	err := bc.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		lastHash = b.Get([]byte("l"))	//通过键"l"拿到区块链顶端区块哈希

		return nil
	})
	if err != nil {
		log.Panic(err)
	}

	//prevBlock := bc.Blocks[len(bc.Blocks)-1]
	//求出新区块
	newBlock := pow.NewBlock(data,lastHash)
	// bc.Blocks = append(bc.Blocks,newBlock)
	//把新区块加入到数据库区块链中
	err = bc.db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		err := b.Put(newBlock.Hash,newBlock.Serialize())
		if err != nil {
			log.Panic(err)
		}
		err = b.Put([]byte("l"),newBlock.Hash)
		bc.tip = newBlock.Hash

		return nil
	})
}

    现在我们产生的区块都会保存在区块链数据库中,我们可以随时打开一条区块链加入新的区块。但是现在我们不能像以前一样通过for-range方式遍历区块链了。所以要打印出区块需要一个迭代器来帮助我们。通过区块链迭代器,我们能以区块能够进入区块链中的顺序进行打印。此外,因为我们不想将所有的块都加载到内存中(因为我们的区块链数据库可能很大!或者现在可以假装它可能很大),我们将会一个一个地读取它们。

//分割线——————迭代器——————
type BlockchainIterator struct {
	currentHash 	[]byte
	db 				*bolt.DB
}
//当需要遍历当前区块链时,创建一个此区块链的迭代器
func (bc *Blockchain) Iterator() *BlockchainIterator {
	bci := &BlockchainIterator{bc.tip,bc.db}

	return bci
}

//迭代器的任务就是返回链中的下一个区块
func (i *BlockchainIterator) Next() *block.Block {
	var Block *block.Block

	//只读方式打开区块链数据库
	err := i.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		//获取数据库中当前区块哈希对应的被序列化后的区块
		encodeBlock := b.Get(i.currentHash)
		//反序列化,获得区块
		Block = block.DeserializeBlock(encodeBlock)

		return nil
	})
	if err != nil {
		log.Panic(err)
	}

	//把迭代器中的当前区块哈希设置为上一区块的哈希,实现迭代的作用
	i.currentHash =Block.PrevBlockHash

	return Block

}

     为了方便我们在命令行界面操作我们的区块链,下面我们来实现CLI交互接口。首先创建一个CLI包,代码如下:

package CLI

import (
	"fmt"
	"os"
	"flag"
	"go_code/A_golang_blockchain/blockchain"
	"go_code/A_golang_blockchain/pow"
	"strconv"
	"log"
)
//首先我们想要拥有这些命令 1.加入区块命令 2.打印区块链命令

//创建一个CLI结构体
type CLI struct {
	BC *blockchain.Blockchain
}


//入口函数
func (cli *CLI) Run() {
	//判断命令行输入参数的个数,如果没有输入任何参数则打印提示输入参数信息
	cli.validateArgs()
	//实例化flag集合
	addBlockCmd := flag.NewFlagSet("addblock",flag.ExitOnError)
	printChainCmd := flag.NewFlagSet("printchain",flag.ExitOnError)

	//注册一个flag标志符
	addBlockData := addBlockCmd.String("data"," ","区块数据")

	switch os.Args[1] {		//os.Args为一个保存输入命令的切片
	case "addblock":
		//解析出"addblock"后面的命令
		err := addBlockCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	case "printchain":
		err := printChainCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	default :
		cli.printUsage()	//提示用户怎么正确输入命令
		os.Exit(1)
	}
	
	//进入被解析出的命令,进一步操作
	if addBlockCmd.Parsed() {
		if *addBlockData == " " {
			addBlockCmd.Usage() 	//如果没有输入标志位data,则提醒用户怎么正确的输入
			os.Exit(1)
		}
		//用户输入正确则进行加入区块的操作
		cli.addBlock(*addBlockData)
	}
	if printChainCmd.Parsed() {
		//打印区块链操作
		cli.printChain()
	}
}

//加入输入格式错误信息提示
func(cli *CLI) printUsage() {
	fmt.Println("Usage:")
	fmt.Println("  addblock -data  区块信息")
	fmt.Println("  printchain - Print all the blocks of the blockchain")
}

//判断命令行参数,如果没有输入参数则显示提示信息
func (cli *CLI) validateArgs() {
	if len(os.Args) < 2 {
		cli.printUsage()
		os.Exit(1)
	}
}

//加入区块函数调用
func (cli *CLI) addBlock(data string) {
	cli.BC.AddBlock(data)
	fmt.Println("成功加入区块...")
}

//打印区块链函数调用
func (cli *CLI) printChain() {
	//这里需要用到迭代区块链的思想
	//创建一个迭代器
	bci := cli.BC.Iterator()

	for {

		block := bci.Next()	//从顶端区块向前面的区块迭代

		fmt.Printf("PrevHash:%xn",block.PrevBlockHash)
		fmt.Printf("Data:%sn",block.Data)
		fmt.Printf("Hash:%xn",block.Hash)
		//验证当前区块的pow
		pow := pow.NewProofOfWork(block)
		boolen := pow.Validate()
		fmt.Printf("POW is %sn",strconv.FormatBool(boolen))
		fmt.Println()
		
		if len(block.PrevBlockHash) == 0 {
			break
		}
	}
}

注意:我之前没有写cli.validateArgs()这个函数来判断命令行输入参数的个数,然后就报错如下:

 报错提示:panic: runtime error: index out of range,原因是如果我们没有输入任何参数的话,os.Args[]这个切片里面只包含main.go这一个默认的参数,此时len(os.Args)=1,但是下面我们用到了os.Args[2:],所以已经超出范围了,所以要报错。后面加上cli.validateArgs()函数来判断,就会有个提示我们要输入参数。

下面是main函数:

package main

import (
	"go_code/A_golang_blockchain/blockchain"
	"go_code/A_golang_blockchain/CLI"
)

func main() {
	//先创建一条区块链
	bc := blockchain.NewBlockchain()
	//这里bc中的字段db由于是小写字母开头,所以我工厂模式了db,由函数Db()返回db
	//程序退出前关闭数据库
	defer bc.Db().Close()

	cli := CLI.CLI{bc}
	cli.Run()

	// //加入区块到区块链中
	// bc.AddBlock("区块01")
	// bc.AddBlock("区块02")

	//打印出区块链中各个区块的信息,并验证各个区块是否合格
	// for _,b := range bc.Blocks {

	// 	fmt.Printf("时间戳:%vn",b.Timestamp)
	// 	fmt.Printf("Data:%sn",b.Data)
	// 	fmt.Printf("上一区块哈希:%xn",b.PrevBlockHash)
	// 	fmt.Printf("Hash:%xn",b.Hash)
	// 	fmt.Printf("Nonce:%vn",b.Nonce)
	// 	//验证当前区块的pow
	// 	pow := pow.NewProofOfWork(b)
	// 	boolen := pow.Validate()
	// 	fmt.Printf("POW is %sn",strconv.FormatBool(boolen))
	// 	fmt.Println()
	// }

}

在main函数里面因为涉及到权限问题,结构体blockchain的db字段不能在外包访问,所以用工厂模式来调用字段db。

 

 下面是这一章节完成后的整个代码:

1、block包:

package block

import (
	"encoding/gob"
	"bytes"
	"log"

)
//区块的结构体
type Block struct {
	Timestamp		int64
	Data			[]byte
	PrevBlockHash	        []byte
	Hash 			[]byte
	Nonce			int
}

//0.3 实现Block的序列化
func (b *Block) Serialize() []byte {
	//首先定义一个buffer存储序列化后的数据
	var result bytes.Buffer
	//实例化一个序列化实例,结果保存到result中
	encoder := gob.NewEncoder(&result)
	//对区块进行实例化
	err := encoder.Encode(b)
	if err != nil {
		log.Panic(err)
	}
	return result.Bytes()
}

//0.3 实现反序列化函数
func DeserializeBlock(d []byte) *Block {
	var block Block
	decoder := gob.NewDecoder(bytes.NewReader(d))
	err := decoder.Decode(&block)
	if err != nil {
		log.Panic(err)
	}
	return &block
}

2、blockchain包:

package blockchain

import (
	"github.com/boltdb/bolt"
	"go_code/A_golang_blockchain/block"
	"go_code/A_golang_blockchain/pow"
	"log"
)
/*
	区块链实现
*/
const dbFile = "blockchain.db"
const blocksBucket = "blocks"
const genesisCoinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
//区块链
type Blockchain struct {
	tip		[]byte
	db 		*bolt.DB
}

//工厂模式db
func(bc *Blockchain) Db() *bolt.DB {
	return bc.db
}

//把区块添加进区块链
func (bc *Blockchain) AddBlock(data string) {
	var lastHash []byte
	//只读的方式浏览数据库,获取当前区块链顶端区块的哈希,为加入下一区块做准备
	err := bc.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		lastHash = b.Get([]byte("l"))	//通过键"l"拿到区块链顶端区块哈希

		return nil
	})
	if err != nil {
		log.Panic(err)
	}

	//prevBlock := bc.Blocks[len(bc.Blocks)-1]
	//求出新区块
	newBlock := pow.NewBlock(data,lastHash)
	// bc.Blocks = append(bc.Blocks,newBlock)
	//把新区块加入到数据库区块链中
	err = bc.db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		err := b.Put(newBlock.Hash,newBlock.Serialize())
		if err != nil {
			log.Panic(err)
		}
		err = b.Put([]byte("l"),newBlock.Hash)
		bc.tip = newBlock.Hash

		return nil
	})
}

//创建创世区块
func GenesisBlock() *block.Block {
	return pow.NewBlock("创世区块",[]byte{})
}
//实例化一个区块链,默认存储了创世区块
func NewBlockchain() *Blockchain {
	//return &Blockchain{[]*block.Block{GenesisBlock()}}
	var tip []byte
	//打开一个数据库文件,如果文件不存在则创建该名字的文件
	db,err := bolt.Open(dbFile,0600,nil)
	if err != nil {
		log.Panic(err)
	}
	//读写操作数据库
	err = db.Update(func(tx *bolt.Tx) error{
		b := tx.Bucket([]byte(blocksBucket))
		//查看名字为blocksBucket的Bucket是否存在
		if b == nil {
			//不存在则从头 创建
			genesis := GenesisBlock()	//创建创世区块
			b,err := tx.CreateBucket([]byte(blocksBucket)) //创建名为blocksBucket的桶
			if err != nil {
				log.Panic(err)
			}
			err = b.Put(genesis.Hash,genesis.Serialize()) //写入键值对,区块哈希对应序列化后的区块
			if err != nil {
				log.Panic(err)
			}
			err = b.Put([]byte("l"),genesis.Hash) //"l"键对应区块链顶端区块的哈希
			if err != nil {
				log.Panic(err)
			}
			tip = genesis.Hash //指向最后一个区块,这里也就是创世区块
		} else {
			//如果存在blocksBucket桶,也就是存在区块链
			//通过键"l"映射出顶端区块的Hash值
			tip = b.Get([]byte("l"))
		}

		return nil
	})

	bc := Blockchain{tip,db}  //此时Blockchain结构体字段已经变成这样了
	return &bc
}

//分割线——————迭代器——————
type BlockchainIterator struct {
	currentHash 	[]byte
	db 				*bolt.DB
}
//当需要遍历当前区块链时,创建一个此区块链的迭代器
func (bc *Blockchain) Iterator() *BlockchainIterator {
	bci := &BlockchainIterator{bc.tip,bc.db}

	return bci
}

//迭代器的任务就是返回链中的下一个区块
func (i *BlockchainIterator) Next() *block.Block {
	var Block *block.Block

	//只读方式打开区块链数据库
	err := i.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		//获取数据库中当前区块哈希对应的被序列化后的区块
		encodeBlock := b.Get(i.currentHash)
		//反序列化,获得区块
		Block = block.DeserializeBlock(encodeBlock)

		return nil
	})
	if err != nil {
		log.Panic(err)
	}

	//把迭代器中的当前区块哈希设置为上一区块的哈希,实现迭代的作用
	i.currentHash =Block.PrevBlockHash

	return Block

}

3、pow包:

package pow

import (
	"fmt"
	"crypto/sha256"
	"strconv"
	"bytes"
	"math/big"
	"go_code/A_golang_blockchain/block"
	"math"
	"time"
)
//在实际的比特币区块链中,加入一个区块是非常困难的事情,其中运用得到的就是工作量证明

//创建一个工作量证明的结构体
type ProofOfWork struct {
	block *block.Block //要证明的区块
	target *big.Int //难度值
}
//声明一个挖矿难度
const targetBits = 10

//实例化一个工作量证明
func NewProofOfWork(b *block.Block) *ProofOfWork {
	target :=  big.NewInt(1)
	target.Lsh(target,uint(256 - targetBits))

	pow := &ProofOfWork{b,target}
	return pow
}

//准备需要进行哈希的数据
func (pow *ProofOfWork) prepareData(nonce int) []byte {
	data := bytes.Join(
		[][]byte{
			pow.block.PrevBlockHash,
			pow.block.Data,
			[]byte(strconv.FormatInt(pow.block.Timestamp,10)),
			[]byte(strconv.FormatInt(targetBits,10)),
			[]byte(strconv.FormatInt(int64(nonce),10)),
		},
		[]byte{},
	)
	return data
}

//进行工作量证明,证明成功会返回随机数和区块哈希
func (pow *ProofOfWork) Run() (int,[]byte) {
	nonce := 0
	var hash [32]byte
	var hashInt big.Int
	for nonce < math.MaxInt64 {
		data := pow.prepareData(nonce)
		hash = sha256.Sum256(data)
		hashInt.SetBytes(hash[:])

		//把哈希后的数据与难度值进行比较
		if hashInt.Cmp(pow.target) == -1 {
			fmt.Printf("工作量证明成功 hash= %x  nonce = %vn",hash,nonce)
			break
		}else{
			nonce ++
		}
	}
	fmt.Println()

	return nonce,hash[:]
}

//实例化一个区块
func NewBlock(data string,prevBlockHash []byte) *block.Block {
	block := &block.Block{time.Now().Unix(),[]byte(data),prevBlockHash,[]byte{},0}
	// block.SetHash()

	pow := NewProofOfWork(block)
	nonce,hash := pow.Run()
	block.Hash = hash
	block.Nonce = nonce
	return block
}

//其他节点验证nonce是否正确
func (pow *ProofOfWork) Validate() bool {
	var hashInt big.Int

	data := pow.prepareData(pow.block.Nonce)
	hash := sha256.Sum256(data)
	hashInt.SetBytes(hash[:])

	isValid := hashInt.Cmp(pow.target) == -1 
	return isValid
}

4、CLI包:

package CLI

import (
	"fmt"
	"os"
	"flag"
	"go_code/A_golang_blockchain/blockchain"
	"go_code/A_golang_blockchain/pow"
	"strconv"
	"log"
)
//首先我们想要拥有这些命令 1.加入区块命令 2.打印区块链命令

//创建一个CLI结构体
type CLI struct {
	BC *blockchain.Blockchain
}


//入口函数
func (cli *CLI) Run() {
	//判断命令行输入参数的个数,如果没有输入任何参数则打印提示输入参数信息
	cli.validateArgs()
	//实例化flag集合
	addBlockCmd := flag.NewFlagSet("addblock",flag.ExitOnError)
	printChainCmd := flag.NewFlagSet("printchain",flag.ExitOnError)

	//注册一个flag标志符
	addBlockData := addBlockCmd.String("data"," ","区块数据")

	switch os.Args[1] {		//os.Args为一个保存输入命令的切片
	case "addblock":
		//解析出"addblock"后面的命令
		err := addBlockCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	case "printchain":
		err := printChainCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	default :
		cli.printUsage()	//提示用户怎么正确输入命令
		os.Exit(1)
	}
	
	//进入被解析出的命令,进一步操作
	if addBlockCmd.Parsed() {
		if *addBlockData == " " {
			addBlockCmd.Usage() 	//如果没有输入标志位data,则提醒用户怎么正确的输入
			os.Exit(1)
		}
		//用户输入正确则进行加入区块的操作
		cli.addBlock(*addBlockData)
	}
	if printChainCmd.Parsed() {
		//打印区块链操作
		cli.printChain()
	}
}

//加入输入格式错误信息提示
func(cli *CLI) printUsage() {
	fmt.Println("Usage:")
	fmt.Println("  addblock -data  区块信息")
	fmt.Println("  printchain - Print all the blocks of the blockchain")
}

//判断命令行参数,如果没有输入参数则显示提示信息
func (cli *CLI) validateArgs() {
	if len(os.Args) < 2 {
		cli.printUsage()
		os.Exit(1)
	}
}

//加入区块函数调用
func (cli *CLI) addBlock(data string) {
	cli.BC.AddBlock(data)
	fmt.Println("成功加入区块...")
}

//打印区块链函数调用
func (cli *CLI) printChain() {
	//这里需要用到迭代区块链的思想
	//创建一个迭代器
	bci := cli.BC.Iterator()

	for {

		block := bci.Next()	//从顶端区块向前面的区块迭代

		fmt.Printf("PrevHash:%xn",block.PrevBlockHash)
		fmt.Printf("Data:%sn",block.Data)
		fmt.Printf("Hash:%xn",block.Hash)
		//验证当前区块的pow
		pow := pow.NewProofOfWork(block)
		boolen := pow.Validate()
		fmt.Printf("POW is %sn",strconv.FormatBool(boolen))
		fmt.Println()
		
		if len(block.PrevBlockHash) == 0 {
			break
		}
	}
}

5、main包:

package main

import (
	"go_code/A_golang_blockchain/blockchain"
	"go_code/A_golang_blockchain/CLI"
)

func main() {
	//先创建一条区块链
	bc := blockchain.NewBlockchain()
	//这里bc中的字段db由于是小写字母开头,所以我工厂模式了db,由函数Db()返回db
	//程序退出前关闭数据库
	defer bc.Db().Close()

	cli := CLI.CLI{bc}
	cli.Run()

	// //加入区块到区块链中
	// bc.AddBlock("区块01")
	// bc.AddBlock("区块02")

	//打印出区块链中各个区块的信息,并验证各个区块是否合格
	// for _,b := range bc.Blocks {

	// 	fmt.Printf("时间戳:%vn",b.Timestamp)
	// 	fmt.Printf("Data:%sn",b.Data)
	// 	fmt.Printf("上一区块哈希:%xn",b.PrevBlockHash)
	// 	fmt.Printf("Hash:%xn",b.Hash)
	// 	fmt.Printf("Nonce:%vn",b.Nonce)
	// 	//验证当前区块的pow
	// 	pow := pow.NewProofOfWork(b)
	// 	boolen := pow.Validate()
	// 	fmt.Printf("POW is %sn",strconv.FormatBool(boolen))
	// 	fmt.Println()
	// }

}

 

注释:上面的代码中注解都是很详细的,运行都能通过,在VScode编辑器中编辑时其中有两处有警告的提示,主要原因是我编写的Block结构体字段和CLI结构体字段的首字母都是大写的,警告提示为:Disable go vet checks for “composite literal uses unkeyed fields”,解释为我所使用的这两个结构体中的都是无键字段,所以警告,这种警告可以忽略的,是go vet的一个提示,并不是错误。

—— 如果以上有什么错误或者疑问欢迎指正,可以加我的联系方式进行讨论:微信(18382255942)——

本人微信公众号,欢迎扫码关注

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/zyj0813/article/details/82053261
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-23 16:51:00
  • 阅读 ( 1770 )
  • 分类:区块链

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢