如何使用Geth在Go中创建DApp

Go Ethereum(或Geth)是以太坊协议的官方Go实现。在以太坊的GitHub库保存了Geth的以太坊客户端和其他工具和开发DApps(分布式应用)库的源代码。

本指南使用Go Ethereum SDK和Rinkeby testnet在Go中编写一个谜语应用程序。你可以在此处找到本指南的示例代码。

为什么用Go写DApps?

编写DApp通常包括两个步骤:

推荐阅读
1

在一天中,一个15岁的青少年赚了0.7 BTC

2

现实游戏集团开始授权数字资产交易平台

1的450
  1. 用Solidity或类似语言编写合约代码。
  2. 编写与部署的智能合约交互的代码。

Go Ethereum SDK允许我们为Go编程语言的第二步编写代码。

编写代码以与智能合约交互通常执行诸如提供用户界面之类的任务,该用户界面允许用户向已部署的合约发送呼叫和消息。这些任务是我们不需要区块链的弹性或分布式容量,或者太昂贵(在美元和计算成本方面)部署到以太坊主网。

Go允许我们使用与Solidity相同的安全功能编写该应用程序代码,以及其他类似的优点:

  • 一个广泛的工具库,可与以太坊网络进行交互。
  • 将Solidity契约代码转换为Go的工具,允许在Go应用程序中直接与合约ABI(应用程序二进制接口)交互。
  • 允许我们使用Go的测试库和Go Ethereum的区块链模拟库编写合约代码和应用程​​序的测试。意味着单元测试我们可以在不连接任何以太坊网络(公共或私人)的情况下运行。

应用结构

在本指南中,我们将编写一个DApp:

  1. 发表一个问题。
  2. 允许用户提交答案。
  3. 允许用户检查他们的答案是否正确。
  4. 如果用户的答案是正确的,请记录他们的地址。

为此,我们需要:

  1. 编写一个智能合约,存储问题,答案,正确回答问题的用户列表以及访问它们的方法。
  2. 编写一个Go应用程序,允许我们部署新合约并加载现有合约。
  3. 编写一个Go应用程序,允许用户:
    1. 阅读问题。
    2. 发送智能合约的答案。
    3. 检查发送的答案是否正确。
    4. 如果发送的答案正确,请记录用户的帐户地址。

建立一个开发环境

要开始使用Go开发DApps,首先安装以太坊工具链

接下来创建一个包含项目的文件夹,对于本指南,我们假设项目位置是/go/geth-dapp-demo

管理Go依赖项

我们使用Go模块来管理此项目的依赖项。要开始使用此项目的Go模块:

打开终端导航到项目文件夹,然后在项目文件夹中运行:

go mod init github.com//geth-dapp-demo

编辑生成的go.mod文件,如下所示,并保存文件:

module github.com//geth-dapp-demo

require (
    github.com/ethereum/go-ethereum v1.8.20
    github.com/joho/godotenv v1.3.0
)

构建应用程序时,Go会自动go.mod使用所需的其他依赖项填充文件。我们现在可以让Go照顾这些。有了这个go.mod文件,Go确保v1.8.20每当我们运行go runor go build命令时都使用Go Ethereum SDK 。

在Infura.io上设置Rinkeby testnet端点

为了使本指南简单明了,我们使用所提供的API以太坊网关Infura.io,而不是运行我们自己的以太坊军节点。要运行Geth节点进行开发,请阅读以太坊101指南

  1. 转到Infura.io并注册一个帐户。
  2. 转到仪表板并单击“ 创建新项目”
  3. 输入项目的名称,然后单击“ 创建”以设置新项目。

新创建的项目应如下所示:

Infura.io的新项目

我们稍后会在部署智能合约时再回过头来看看。

目前,我们需要项目的以太坊API网关端点的URL。从ENDPOINT下拉菜单中选择“RINKEBY” ,并记下它下面显示的URL。它应该如下所示:

https://rinkeby.infura.io/v3/

重要:确保Go代码中使用的端点指向Rinkeby testnet。如果我们使用指向以太坊主网的端点,我们会花费真正的以太坊来测试应用程序。

在名为的项目文件夹中创建一个文件.env不要将此文件提交给Git或任何其他版本控制系统(VCS)。编辑.env文件并进入项目的以太坊API网关端点:

GATEWAY="https://rinkeby.infura.io/v3/"

保存文件。我们稍后会在Go应用程序中使用它。

注意:使用第三方提供商连接到以太坊网络意味着我们信任所有交易以及通过它发送的任何以太坊。如果我们不使用第三方提供商,我们必须运行并托管我们自己的以太坊API网关,或依靠用户连接到他们自己的以太坊节点。

设置以太坊帐户

我们需要一个以太坊帐户来部署我们的智能合约。要创建新的以太坊帐户,请运行以下命令并按照屏幕上的说明操作:

geth --datadir . account new

此命令keystore在当前目录中创建一个文件夹。在其中,是一个新帐户的密钥库文件UTC----不要将密钥库提交给VCS。

我们需要这个密钥库文件和加密货币来部署智能合约。.env在前面创建的文件中保存密钥库文件和加密货币的位置:

GATEWAY="..."
KEYSTORE="$HOME/.ethereum/keystore/UTC--2018-12-30T12-29-11.490098600Z--"
KEYSTOREPASS=""

部署合约并进行合约调用; 我们需要我们的帐户包含Rinkeby testnet以太坊。通过访问https://faucet.rinkeby.io并按照其中的说明获取该帐户的testnet以太坊。

编写和编写智能合约

我们都准备好了首先,我们写下智能合约:

  1. 在项目目录中创建一个新文件夹并为其命名quiz
  2. 在其中,创建一个名为的文件quiz.sol并添加以下代码:
pragma solidity >=0.5.2 <0.6.0;

contract Quiz {
    string public question;
    bytes32 internal _answer;
    mapping (address => bool) internal _leaderBoard;

    constructor(string memory _qn, bytes32 _ans) public {
        question = _qn;
        _answer = _ans;
    }

    function sendAnswer(bytes32 _ans) public returns (bool){
        return _updateLeaderBoard(_answer == _ans);
    }

    function _updateLeaderBoard(bool ok) internal returns (bool){
        _leaderBoard[msg.sender] = ok;
        return true;
    }

    function checkBoard() public view returns (bool){
        return _leaderBoard[msg.sender];
    }
}

我们将简要介绍我们的智能合约代码所做的事情,有关在Solidity中编写智能合约的更多信息,请阅读本指南

首先,我们设置要在合约上存储的数据类型。

string public question:存储我们想要询问用户的问题。将此设置为public具有Solidity会在合约编译时自动为其生成getter函数。这允许我们用contractInstance.question()方法读取这个变量的内容。因为getter不会在EVM上调用代码执行,所以它们不需要运行气体。

bytes32 internal _answer:存储我们问题的答案。我们设置了一个internal修饰符,这意味着只能从此合约中访问此变量。

mapping (address=>bool) internal _leaderBoard:存储用户帐户的哈希映射和布尔值,告诉我们给定帐户是否正确回答了问题。我们还设置了这个状态变量,internal以防止外部调用者修改其内容。

接下来,constructor在我们部署合约时调用它,我们给它一个问题_qn和一个答案_ans

我们将_qn字符串视为字符串,因为我们的意思是任何与合约交互的人都可以轻松阅读。

我们的答案_ans设置为32字节(bytes32)的固定片段,因为我们希望将其存储为keccak256哈希。哈希值_ans掩盖了它,使其在合约源或事务日志中不可读。

sendAnswer()允许我们发送合约答案。发送给合约的答案必须是32字节的keccak256哈希,我们将其_answer与合约中存储的值进行比较。如果值匹配,我们更新我们leaderBoard以显示进行此函数调用的帐户已正确回答。

_updateLeaderBoard()获取true / false值并将_leaderBoard用户帐户的映射条目设置为该值。它是一个internal函数,可以防止外部调用者任意修改_leaderBoard映射。

checkBoard()检查合约是否记录了用户正确回答。当前用户的以太坊帐户由设置以太坊帐户中的KEYSTORE环境变量设置

现在我们已经完成了Solidity合约,我们需要将其编译为ABI JSON规范和合约二进制文件。然后,我们将从这些文件生成Go绑定文件,并将其导入Go DApp。

我们将使用solcabigen执行此操作。运行以下命令以编译合约:

solc --abi --bin quiz.sol -o build

此命令创建一个build包含文件Quiz.abi和的文件夹Quiz.bin

接下来,生成Go绑定文件。在“测验”目录中,运行:

abigen --abi="build/Quiz.abi" --bin="build/Quiz.bin" --pkg=quiz --out="quiz.go"

此命令生成一个Go文件,其中包含我们可以导入到Go代码中的智能合约的绑定。

围棋代码

连接到Rinkeby网络并获得帐户余额

我们将通过使用我们之前设置的Infura.io网关端点初始化与Rinkeby网络的连接来开始编写Go DApp 。

在项目根目录中,创建一个新main.go文件并添加以下代码:

package main

import (
    "context"
    "log"
    "fmt"

    "github.com/ethereum/go-ethereum"
    "github.com/joho/godotenv"
)

var myenv map[string]string

const envLoc = ".env"

func loadEnv() {
    var err error
    if myenv, err = godotenv.Read(envLoc); err != nil {
        log.Printf("could not load env from %s: %v", envLoc, err)
    }
}

func main(){
    loadEnv()

    ctx := context.Background()

    client, err := ethclient.Dial(os.Getenv("GATEWAY"))
    if err != nil {
        log.Fatalf("could not connect to Ethereum gateway: %vn", err)
    }
    defer client.Close()

    accountAddress := common.HexToAddress("")
    balance, _ := client.BalanceAt(ctx, accountAddress, nil)
    fmt.Printf("Balance: %dn",balance)
}

设置以太坊帐户步骤替换为以太坊帐户的地址。

在这里,我们:

首先使用包将.env文件中的数据加载到地图中,我们将其设置为文件中的依赖项。myenvgodotenvgo.mod

然后我们可以访问我们.env文件中设置的值myenv["KEYNAME"]。例如,使用访问该GATEWAYmyenv["GATEWAY"]

请注意,我们还编写了一个函数loadEnv(),我们可以在每个函数范围的开头调用它。通过loadEnv()在每个使用环境变量的函数的开头调用,我们确保.env在应用程序运行时捕获对文件的任何更新。

接下来,我们通过调用建立与Infura.io Rinkeby网关的连接ethclient.Dial("")。这适用于TCP(HTTP / S)和IPC(/geth.ipc)端点。然后通过调用client.GetBalance(ctx, accountAddress, nil)将我们的以太坊地址从十六进制字符串48fddc985ecc605127f1a1c098c817778187637c转换为common.Address类型,然后传递给它GetBalance()并打印结果,获得我们的以太坊帐户的余额GetBalance()

通过go run main.go在终端中运行来测试应用程序。如果它打印以太坊帐户的余额,则应用程序已成功从.env文件加载配置并向Rinkeby网络发送消息调用。

现在我们知道我们的ethclient.Dial()呼叫有效,我们将不需要GetBalance()呼叫。将其删除main(),以便main()块看起来像这样:

func main(){
    loadEnv()

    ctx := context.Background()

    client, err := ethclient.Dial(os.Getenv("GATEWAY"))
    if err != nil {
        log.Fatalf("could not connect to Ethereum gateway: %vn", err)
    }
    defer client.Close()

}

创建会话

会话是盘点器,允许我们进行合约调用,而不必传递授权凭证和不断调用参数。会话盘点:

  • 合约实例,
  • bind.CallOpts包含进行合约调用的选项的结构,
  • bind.TransactOpts包含授权凭据和参数的结构,用于创建有效的以太坊事务。

创建会话允许我们在合约实例上进行调用,如下所示:

auth, _ := bind.NewTransactor(keystorefile, keystorepass)
session.TransactOpts = auth

// This calls the contract method sendAnswer(),
// which returns the question that we've set
// for our deployed contract.
session.SendAnswer(answer)
session.Question()

与每次我们进行合约调用或交易时必须传入bind.CallOptsbind.TransactOpts结构相反:

auth, _ := bind.NewTransactor(keystorefile, keystorepass)
contractInstance.SendAnswer(&bind.TransactOpts{
        From: auth.From,
        Nonce: nil,           // nil uses nonce of pending state
        Signer: auth.Signer,
        Value: big.NewInt(0),
        GasPrice: nil,        // nil automatically suggests gas price
        GasLimit: 0,          // 0 automatically estimates gas limit
    },
    answer,
    )
contractInstance.Question(&bind.CallOpts{
    Pending: true,
    From: auth.From,
    Context: context.Background(),
})
contractInstance.CheckBoard(&bind.CallOpts{
    Pending: true,
    From: auth.From,
    Context: context.Background(),
})

相反,我们在创建新会话时执行一次:

auth, _ := bind.NewTransactor(keystorefile, keystorepass)
session := quiz.QuizSession{
    Contract: contractInstance,
    CallOpts: bind.CallOpts{
        Pending: true,        // Acts on pending state if set to true
        From: auth.From,
        Context: context.Background(),
    },
    TransactOpts: bind.TransactOpts{
        From: auth.From,
        Nonce: nil,           // nil uses nonce of pending state
        Signer: auth.Signer,
        Value: big.NewInt(0),
        GasPrice: nil,        // nil automatically suggests gas price
        GasLimit: 0,          // 0 automatically estimates gas limit
    },
}

session.SendAnswer(answer)
session.Question()
session.CheckBoard()

注: bind.NewTransactor()返回一个bind.TransactOpts结构用From,并Signer与来自密钥库文件信息字段填写,其他字段填写与安全默认值。我们可以按原样使用它进行交易。例如,contractInstance.SendAnswer(auth, answer)也适用于上面的示例。

让我们创建一个NewSession()创建新的可用会话并返回它的函数,将其添加到main.go文件的底部:

func NewSession(ctx context.Context) (session quiz.QuizSession) {
    loadEnv()
    keystore, err := os.Open(myenv["KEYSTORE"])
    if err != nil {
        log.Printf(
            "could not load keystore from location %s: %vn",
            myenv["KEYSTORE"],
            err,
        )
    }
    defer keystore.Close()

    keystorepass := myenv["KEYSTOREPASS"]
    auth, err := bind.NewTransactor(keystore, keystorepass)
    if err != nil {
        log.Printf("%sn", err)
    }

    // Return session without contract instance
    return quiz.QuizSession{
        TransactOpts: *auth,
        CallOpts: bind.CallOpts{
            From:    auth.From,
            Context: ctx,
        },
    }
}

在这里,我们:

  • 从中加载我们的环境变量.env
  • 从我们的密钥库文件中读取。
  • 从环境变量获取我们的密钥库加密货币KEYSTOREPASS
  • 通过bind.NewTransactor()调用创建一个新的交易者。
  • quiz.QuizSession使用我们新创建的transactor和一个CallOpts带有一些默认值的结构来构造并返回一个新结构。

然后,我们可以在main()以下位置创建新会话:

func main(){
    // ...
    session := NewSession(context.Background())
}

我们没有为Contract我们返回的会话中的字段指定值NewSession()session在我们获得合约实例之后,我们会在退货时执行此操作,当我们在区块链上部署新合约或我们加载现有合约时。

部署并加载合约

现在我们已经创建了一个新会话,我们需要为它分配一个契约实例。

我们通过部署合约或从合约地址加载现有合约来获得合约实例。

我们将编写两个函数来执行这些任务:

// NewContract deploys a contract if no existing contract exists
func NewContract(session quiz.QuizSession, client *ethclient.Client, question string, answer string) (quiz.QuizSession) {
    loadEnv()

    // Hash answer before sending it over Ethereum network.
    contractAddress, tx, instance, err := quiz.DeployQuiz(&session.TransactOpts, client, question, stringToKeccak256(answer))
    if err != nil {
        log.Fatalf("could not deploy contract: %vn", err)
    }
    fmt.Printf("Contract deployed! Wait for tx %s to be confirmed.n", tx.Hash().Hex())

    session.Contract = instance
    updateEnvFile("CONTRACTADDR", contractAddress.Hex())
    return session
}

// LoadContract loads a contract if one exists
func LoadContract(session quiz.QuizSession, client *ethclient.Client) quiz.QuizSession {
    loadEnv()

    addr := common.HexToAddress(myenv["CONTRACTADDR"])
    instance, err := quiz.NewQuiz(addr, client)
    if err != nil {
        log.Fatalf("could not load contract: %vn", err)
        log.Println(ErrTransactionWait)
    }
    session.Contract = instance
    return session
}

// Utility functions

// stringToKeccak256 converts a string to a keccak256 hash of type [32]byte
func stringToKeccak256(s string) [32]byte {
    var output [32]byte
    copy(output[:], crypto.Keccak256([]byte(s))[:])
    return output
}

// updateEnvFile updates our env file with a key-value pair
func updateEnvFile(k string, val string) {
    myenv[k] = val
    err := godotenv.Write(myenv, envLoc)
    if err != nil {
        log.Printf("failed to update %s: %vn", envLoc, err)
    }
}

双方NewContract()LoadContract()创建合约的实例,我们然后分配到Contract与会话session.Contract = instance。然后我们返回会话。

部署新合约

我们的NewContract()函数作为参数:

  • session quiz.QuizSession:一个会话,我们在Create session中初始化。
  • client *ethclient.Client:我们初始化的客户端对象main()
  • question string:包含我们希望用户回答的问题的字符串。
  • answer string:问题的答案,我们将其作为字符串参数。

我们必须找到一种方法将字符串作为questionanswer参数传递给我们的合约,但我们不想硬编码我们的答案或提交包含VCS答案的文件。如果我们这样做,查看合约源或我们的DApp源代码的用户将能够找到answer以纯文本形式存储的预期值。

我们也不希望answer将合约的值作为纯文本发送给合约,因为广播到网络的所有事务的内容都记录为事务的有效负载的一部分。在查看事务的有效负载时,以纯文本形式发送的任何值都将按原样显示。

0x445d51fc29741b261f392936970b3c842e922dec841023ca40e248b9d3a2ba19在Rinkeby网络上查看此示例。

答案存储为纯文本

为了解决这个问题,我们做了两件事:

我们已经从.env文件中加载了值,因此我们可以使用它来存储我们的值questionanswer值。

添加QUESTIONANSWER键值对。对以下内容进行以下更改.env

GATEWAY="..."
KEYSTORE="..."
KEYSTOREPASS="..."
QUESTION="this is a question"
ANSWER="this is the answer"

在我们完成之后,我们可以分别使用和加载代码中的questionanswer值。myenv["QUESTION"]myenv["ANSWER"]

接下来,answer在将其作为session.DeployQuiz()调用的一部分发送之前,将其编码为Keccak256哈希值。我们可以使用stringToKeccak256()将给定字符串转换为类型的keccak256哈希的效用函数[32]byte

我们现在可以运行quiz.DeployQuiz()并获取合约地址contractAddress,事务对象tx和合约实例instance。我们将合约实例分配给session.Contract并返回现在完全形成的会话。

我们还打印交易地址,用户可以在Etherscan上查看以检查交易进度。

最后,我们需要保存已部署合约的地址。我们.env使用该godotenv.Write()方法将其保存到我们的文件中。在这里,我们使用另一个实用功能updateEnvFile()来帮助我们这样做。updateEnvFile()执行以下操作:

  1. CONTRACTADDR向我们的myenv地图添加一个键,并将合约地址十六进制分配给它。
  2. 调用godotenv.Write(myenv, envLoc)将更新的myenv地图写入我们的.env文件。

加载现有合约

LoadContract()函数还将a sessionclientinstance作为参数。然后,它尝试通过查找文件中的CONTRACTADDR条目来加载现有合约.env

如果文件CONTRACTADDR中不存在a .env,我们将不知道在区块链中找到合约的位置,因此移除该功能。

否则,调用quiz.NewQuiz()以创建新的合约实例并将其分配给session.Contract

如果合约不存在,则部署

NewContract()如果我们还没有区块链的现有合约,我们只想打电话。

为此,我们编写if语句以确保NewContract()仅在CONTRACTADDR未在我们的.env文件中设置时调用,并且LoadContract()仅在我们可以找到非空CONTRACTADDR值时运行:

func main() {
    // ...
    // Load or Deploy contract, and update session with contract instance
    if myenv["CONTRACTADDR"] == "" {
        session = NewContract(session, client, myenv["QUESTION"], myenv["ANSWER"])
    }

    // If we have an existing contract, load it; if we've deployed a new contract, attempt to load it.
    if myenv["CONTRACTADDR"] != "" {
        session = LoadContract(session, client)
    }
}

注意:一旦我们做到这一点,DAPP尝试加载从价值合约CONTRACTADDR.env作为值不是一个空字符串的文件,只要("")。要强制DApp部署新合约,请删除文件中的CONTRACTADDR条目.env,或将其设置为空字符串("")。

与合约互动

现在我们有一个合约实例,我们可以使用它来进行合约调用。

标记为生成publicquiz.go文件中的任何函数或状态变量abigen都可以quiz.go作为我们可以在契约实例上调用的方法使用。

例如,因为我们有以下代码quiz.sol

function sendAnswer(bytes32 _ans) public returns (bool)

quiz.go在我们的Go DApp中导入允许我们调用:

contractInstance.SendAnswer(&bind.CallOpts, answer)

请记住,我们希望使用Go DApp 执行以下操作

  • 阅读问题。
  • 发送智能合约的答案。
  • 检查发送的答案是否正确。
  • 如果发送的答案正确,请记录用户的帐户地址。

要执行这些任务,我们将以下函数添加到main.go文件的底部:

//// Contract interaction

// ErrTransactionWait should be returned/printed when we encounter an error that may be a result of the transaction not being confirmed yet.
const ErrTransactionWait = "if you've just started the application, wait a while for the network to confirm your transaction."

// readQuestion prints out question stored in contract.
func readQuestion(session quiz.QuizSession) {
    qn, err := session.Question()
    if err != nil {
        log.Printf("could not read question from contract: %vn", err)
        log.Println(ErrTransactionWait)
        return
    }
    fmt.Printf("Question: %sn", qn)
    return
}

// sendAnswer sends answer to contract as a keccak256 hash.
func sendAnswer(session quiz.QuizSession, ans string) {
    // Send answer
    txSendAnswer, err := session.SendAnswer(stringToKeccak256(ans))
    if err != nil {
        log.Printf("could not send answer to contract: %vn", err)
        return
    }
    fmt.Printf("Answer sent! Please wait for tx %s to be confirmed.n", txSendAnswer.Hash().Hex())
    return
}

// checkCorrect makes a contract message call to check if
// the current account owner has answered the question correctly.
func checkCorrect(session quiz.QuizSession) {
    win, err := session.CheckBoard()
    if err != nil {
        log.Printf("could not check leaderboard: %vn", err)
        log.Println(ErrTransactionWait)
        return
    }
    fmt.Printf("Were you correct?: %vn", win)
    return
}

在这里,我们编写三个辅助函数来盘点我们的契约调用:

  • readQuestion(session quiz.QuizSession) 读取我们存储在已部署的智能合约上的问题,并将其打印出来。
  • sendAnswer(session quiz.QuizSession, ans string) 将答案作为字符串,将其编码为keccak256哈希,并将其发送到智能合约。
  • checkCorrect(session quiz.QuizSession) 检查当前用户是否在我们的智能合约上记录为已发送正确答案。

现在,我们可以调用这些函数main()来与已部署的智能合约进行交互。

写一个简单的CLI

接下来,我们将编写一个简单的命令行界面(CLI),以允许我们的用户:

  1. 阅读问题。
  2. 发送答案。
  3. 检查他们的答案是否正确。

要实现此功能,请将以下内容添加到main()块的底部:

// Loop to implement simple CLI
for {
    fmt.Printf(
        "Pick an option:n" + "" +
            "1. Show question.n" +
            "2. Send answer.n" +
            "3. Check if you answered correctly.n" +
            "4. Exit.n",
    )

    // Reads a single UTF-8 character (rune)
    // from STDIN and switches to case.
    switch readStringStdin() {
    case '1':
        readQuestion(session)
        break
    case '2':
        fmt.Println("Type in your answer")
        sendAnswer(session, readStringStdin())
        break
    case '3':
        checkCorrect(session)
        break
    case '4':
        fmt.Println("Bye!")
        return
    default:
        fmt.Println("Invalid option. Please try again.")
        break
    }
}

然后,将以下帮助函数添加到main.go文件的底部:

// readStringStdin reads a string from STDIN and strips any trailing n characters from it.
func readStringStdin() string {
    reader := bufio.NewReader(os.Stdin)
    inputVal, err := reader.ReadString('n')
    if err != nil {
        log.Printf("invalid option: %vn", err)
        return ""
    }

    output := strings.TrimSuffix(inputVal, "n") // Important!
    return output
}

当我们go run main.go在终端中运行Go DApp时,readStringStdin()调用bufio.NewReader(io.Stdin),暂停程序并等待用户在命令行上输入值。然后它接受该输入,处理它,并将其作为Go应用程序可以使用的值返回。

我们使用执行以下操作的无限for循环来实现CLI :

  1. 打印出使用CLI的快速说明。
  2. 输入一个switch语句,该语句从命令行上的用户输入读取,并执行给定case的适当rune接收。
  3. 当用户选择一个选项时,该代码将case运行并forbreak调用时返回到循环的顶部。

运行应用程序

恭喜我们完成了测验DApp

在测试应用程序之前,请检查该.env文件是否包含Go DApp需要运行的值。它应该看起来像这样:

GATEWAY="https://rinkeby.infura.io/v3/"
KEYSTORE="/keystore/UTC--2019-01-14T13-58-48.439126200Z--"
KEYSTOREPASS=""
QUESTION="this is a question"
ANSWER="this is the answer"

要运行Go DApp,请输入终端:

go run main.go

或者,通过运行以下命令构建并运行Go DApp:

go build main.go
./main

限制

我们的DApp就是我们可以用智能合约和Go DApp做的一个简单例子。因为我们试图保持这个例子的简单,我们的DApp有一些限制:

我们的DApp不知道交易是否完成。这就是为什么我们需要单独的函数来发送区块链的答案,另一个来检查答案是否正确。我们可以通过让一个进程在我们的合约地址监听区块链上的任何事件来实现这一点,但这超出了本指南的范围。

我们的用户不能只运行DApp,它的工作原理。他们需要指定密钥库文件,并确保他们已准备好与之交互的已部署合约。我们可以通过添加CLI选项来更正此问题,这些选项允许用户输入配置这些参数的值。

我们的DApp假设运行它的用户是(1)部署合约的同一个人,(2)回答问题。理想情况下,部署合约的DApp和与合约交互的DApp应该是分开的。

原文:https://kauri.io/article/60a36c1b17d645939f63415218dc24f9/v2/creating-a-dapp-in-go-with-geth

关注我们:Twitter | Facebook | Linkedin | Medium | Telegram | Weibo | WeChat