编写银行 API

已完成

现在,我们已经构建了网上银行的核心逻辑,接下来让我们构建一个 Web API,以通过浏览器(甚至是命令行)对该逻辑进行测试。 目前,我们不使用数据库来持久保存数据,因此必须创建一个全局变量,以便将所有帐户存储在内存中。

此外,我们将跳过测试部分,以免本指南的操作持续太长时间。 理想情况下,你应该遵循我们在构建核心程序包时遵循的相同方法,在编写代码之前编写测试。

在内存中设置帐户

我们将为帐户使用在程序启动时创建的内存映射,而不是使用数据库来持久保存数据。 另外,我们将使用映射通过帐号来访问帐户信息。

转到 $GOPATH/src/bankapi/main.go 文件,添加以下代码来创建全局 accounts 变量并使用一个帐户来初始化该变量。 (此代码类似于我们之前创建测试时添加的代码。)

package main

import (
    "github.com/msft/bank"
)

var accounts = map[float64]*bank.Account{}

func main() {
    accounts[1001] = &bank.Account{
        Customer: bank.Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number: 1001,
    }
}

确保位于 $GOPATH/src/bankapi/ 位置。 使用 go run main.go 来运行程序以确保没有任何错误。 此程序目前不做任何其他事情,因此我们将添加逻辑来创建一个 Web API。

公开对账单方法

正如你在以前的模块中看到的那样,采用 Go 创建 Web API 非常简单。 我们将继续使用 net/http 程序包。 我们还将使用 HandleFuncListenAndServe 函数来公开终结点并启动服务器。 HandleFunc 函数需要一个你要公开的 URL 路径的名称,以及包含该终结点的逻辑的函数的名称。

首先,我们将公开用来输出某个帐户的对账单的功能。 将以下函数复制并粘贴到 main.go 中:

func statement(w http.ResponseWriter, req *http.Request) {
    numberqs := req.URL.Query().Get("number")

    if numberqs == "" {
        fmt.Fprintf(w, "Account number is missing!")
        return
    }

    if number, err := strconv.ParseFloat(numberqs, 64); err != nil {
        fmt.Fprintf(w, "Invalid account number!")
    } else {
        account, ok := accounts[number]
        if !ok {
            fmt.Fprintf(w, "Account with number %v can't be found!", number)
        } else {
            fmt.Fprintf(w, account.Statement())
        }
    }
}

statement 函数的第一个重点是接收用于将响应写回到浏览器的对象 (w http.ResponseWriter)。 它还会接收用于访问 HTTP 请求中的信息的请求对象 (req *http.Request)。

然后请注意,我们使用 req.URL.Query().Get() 函数从查询字符串读取参数。 此参数是我们将通过 HTTP 调用发送的帐号。 我们将使用该值来访问帐户映射并获取其信息。

由于我们要从用户那里获取数据,因此应包括一些验证以避免出现故障。 当我们知道自己具有有效帐号后,就可以调用 Statement() 方法,并将它返回的字符串输出到浏览器 (fmt.Fprintf(w, account.Statement()))。

现在,修改 main() 函数,使其类似于:

func main() {
    accounts[1001] = &bank.Account{
        Customer: bank.Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number: 1001,
    }

    http.HandleFunc("/statement", statement)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

如果在运行程序 (go run main.go) 时未看到任何错误或输出,则表明它在正常运行。 打开 Web 浏览器并输入 URL http://localhost:8000/statement?number=1001,或在程序运行时在另一个 shell 中运行以下命令:

curl http://localhost:8000/statement?number=1001

应会看到以下输出:

1001 - John - 0

公开存款方法

接下来,我们将继续使用相同的方法来公开存款方法。 在本例中,我们要向内存中的帐户增加资金。 每次调用 Deposit() 方法时,余额都应当增加。

在主程序中,添加一个 deposit() 函数,如下所示。 此函数从查询字符串中获取帐号,验证 accounts 映射中是否存在该帐户,验证要存入的金额是否为有效的数字,然后调用 Deposit() 方法。

func deposit(w http.ResponseWriter, req *http.Request) {
    numberqs := req.URL.Query().Get("number")
    amountqs := req.URL.Query().Get("amount")

    if numberqs == "" {
        fmt.Fprintf(w, "Account number is missing!")
        return
    }

    if number, err := strconv.ParseFloat(numberqs, 64); err != nil {
        fmt.Fprintf(w, "Invalid account number!")
    } else if amount, err := strconv.ParseFloat(amountqs, 64); err != nil {
        fmt.Fprintf(w, "Invalid amount number!")
    } else {
        account, ok := accounts[number]
        if !ok {
            fmt.Fprintf(w, "Account with number %v can't be found!", number)
        } else {
            err := account.Deposit(amount)
            if err != nil {
                fmt.Fprintf(w, "%v", err)
            } else {
                fmt.Fprintf(w, account.Statement())
            }
        }
    }
}

请注意,此函数遵循类似的方法来获取和验证它从用户那里收到的数据。 我们还直接在 if 语句中声明和使用变量。 最后,将一些资金添加到帐户后,我们将输出对账单来查看新的帐户余额。

现在,你应公开一个调用 deposit 函数的 /deposit 终结点。 将 main() 函数修改为如下所示的形式:

func main() {
    accounts[1001] = &bank.Account{
        Customer: bank.Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number: 1001,
    }

    http.HandleFunc("/statement", statement)
    http.HandleFunc("/deposit", deposit)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

如果在运行程序 (go run main.go) 时未看到任何错误或输出,则表明它在正常运行。 打开 Web 浏览器并输入 URL http://localhost:8000/deposit?number=1001&amount=100,或在程序运行时在另一个 shell 中运行以下命令:

curl "http://localhost:8000/deposit?number=1001&amount=100"

应会看到以下输出:

1001 - John - 100

如果多次进行相同的调用,则帐户余额将继续增加。 尝试确认内存中的 accounts 映射在运行时是否更新。 如果你停止程序,则你存入的任何存款都会丢失,但在此初始版本中这是意料之中的情况。

公开取款方法

最后,让我们公开从帐户中取款的方法。 同样,我们将先在主程序中创建 withdraw 函数。 此函数将验证帐号信息,取款并输出从核心程序包收到的任何错误。 在主程序中添加以下函数:

func withdraw(w http.ResponseWriter, req *http.Request) {
    numberqs := req.URL.Query().Get("number")
    amountqs := req.URL.Query().Get("amount")

    if numberqs == "" {
        fmt.Fprintf(w, "Account number is missing!")
        return
    }

    if number, err := strconv.ParseFloat(numberqs, 64); err != nil {
        fmt.Fprintf(w, "Invalid account number!")
    } else if amount, err := strconv.ParseFloat(amountqs, 64); err != nil {
        fmt.Fprintf(w, "Invalid amount number!")
    } else {
        account, ok := accounts[number]
        if !ok {
            fmt.Fprintf(w, "Account with number %v can't be found!", number)
        } else {
            err := account.Withdraw(amount)
            if err != nil {
                fmt.Fprintf(w, "%v", err)
            } else {
                fmt.Fprintf(w, account.Statement())
            }
        }
    }
}

现在,在 main() 函数中添加 /withdraw 终结点以公开你在 withdraw() 函数中实现的逻辑。 将 main() 函数修改为如下所示的形式:

func main() {
    accounts[1001] = &bank.Account{
        Customer: bank.Customer{
            Name:    "John",
            Address: "Los Angeles, California",
            Phone:   "(213) 555 0147",
        },
        Number: 1001,
    }

    http.HandleFunc("/statement", statement)
    http.HandleFunc("/deposit", deposit)
    http.HandleFunc("/withdraw", withdraw)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

如果在运行程序 (go run main.go) 时未看到任何错误或输出,则表明它在正常运行。 打开 Web 浏览器并输入 URL http://localhost:8000/withdraw?number=1001&amount=100,或在程序运行时在另一个 shell 中运行以下命令:

curl "http://localhost:8000/withdraw?number=1001&amount=100"

应会看到以下输出:

the amount to withdraw should be greater than the account's balance

请注意,我们收到的错误来自核心程序包。 当程序启动时,帐户余额为零。 因此,你无法提取任何金额的存款。 多次调用 /deposit 终结点来添加资金,并再次调用 /withdraw 终结点来确认它正常运行:

curl "http://localhost:8000/deposit?number=1001&amount=100"
curl "http://localhost:8000/deposit?number=1001&amount=100"
curl "http://localhost:8000/deposit?number=1001&amount=100"
curl "http://localhost:8000/withdraw?number=1001&amount=100"

应会看到以下输出:

1001 - John - 200

就这么简单! 你已创建了一个 Web API,用于公开你从头构建的一个程序包中的功能。 请转到下一部分继续进行练习。 这次,你将编写自己的解决方案来完成一项挑战。