Designing APIs in Go
Designing our social network API
由于我们的应用程序是social network.我们不仅关注于用户信息,而需要连接它们,并且发送消息。我们需要确保新的用户可以与已经存在的群共享消息,创建和修改连接,以及处理身份验证。
Endpoints | Method | Description |
---|---|---|
/api/users | GET | Return a list of users with optional parameters |
/api/users | POST | Create a user |
/api/users/XXX | PUT | Update a user’s information |
/api/users/XXX | DELETE | Delete a user |
/api/connections | GET | Return a list of connections based on users |
/api/connections | POST | Create a connection between users |
/api/connections/XXX | PUT | Modify a connection |
/api/connections/XXX | DELETE | Remove a connection between users |
/api/statuses | GET | Get a list of statuses |
/api/statuses | POST | Create a status |
/api/statuses/XXX | PUT | Update a status |
/api/statuses/XXX | DELETE | Delete a status |
/api/comments | GET | Get list of comments |
/api/comments | POST | Create a comment |
/api/comments/XXX | PUT | Update a comment |
/api/comments/XXX | DELETE | Delete a comment |
XXX表示URL端点中的唯一标识符。
以上的端点直接反应了数据库对应的表,但有的API是通过OAuth:
Endpoint | Method | Description |
---|---|---|
/api/oauth/authorize | GET | Returns a list of users with optional parameters |
/api/oauth/token | POST | Creates a user |
/api/oauth/revoke | PUT | Updates a user’s information |
如果你还不熟悉OAuth, 请不用担心,我们会有一章专门讲OAuth.
处理我们的API版本
如果你花了大量的时间在互联网上处理service和API, 你会发现有很多种服务来处理它们的API版本。
但有的方法会打破向前和向后的兼容性。你应该避免这种方式。默认情况下,使用版本作为URI:/api/v1.1/users. 你会发现这非常常见,比如Twitter的API处理请求. 这种方法也有优点和缺点,所以你应该考虑你的URI方法中潜在缺点。
明确定义API版本时,这意味着用户总是能够找到他要求的版本。这意味着任何API的升级都不会影响到之前的版本。最坏的是,用户可能不知道哪个版本是最新的,用来检查和校验描述性的API消息。
我们知道, Go不允许条件导入。即,我们不能有以下的代码
if version == 1 {
import "v1"
} else if version == 2 {
import "v2"
}
假设我们的文件结构如下
socialnetwork.go
/{GOPATH}/github.com/nkozyra/gowebservice/v1.go
/{GOPATH}/github.com/nkozyra/gowebservice/v2.go
我们可以通过以下方式进行导入
import "github.com/nkozyra/gowebservice/v1"
import "github.com/nkozyra/gowebservice/v2"
当然,我们必须在我们的应用程序中必须使用这些包,否则Go会触发一个编译错误。
以下是一个例子,操作多个版本
package main
import
(
"nathankozyra.com/api/v1"
"nathankozyra.com/api/v2"
)
func main() {
v := 1
if v == 1 {
v1.API()
// do stuff with API v1
} else {
v2.API()
// do stuff with API v2
}
}
但不幸的是,这打破了编程的一个核心原则:不能复制代码。虽然这不是硬性的规定,但复杂代码会导致功能的碎片和其它头痛的问题。我们可以把主要的方法跨版本方法,某种程度上可以解决这个问题。
在这个例子中,每一个我们的API版本都会导入我们标准的API,serving-and-routing文件,如下所示
package v2
import
(
"nathankozyra.com/api/api"
)
type API struct {
}
func main() {
api.Version = 1
api.StartServer()
}
表面上看上去v2是一个不同的版本。本质上,它只是包装了我们的共享数据,比如数据库连接,数据编码等等。
为了详细说明,我们看看api.go的代码
import (
"database/sql"
"encoding/json"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/gorilla/mux"
"net/http"
"log"
)
var Database *sql.DB
type Users struct {
Users []User `json:"users"`
}
type User struct {
ID int "json:id"
Name string "json:username"
Email string "json:email"
First string "json:first"
Last string "json:last"
}
func StartServer() {
db, err := sql.Open("mysql", "root@/social_network")
if err != nil {
}
Database = db
routes := mux.NewRouter()
http.Handle("/", routes)
http.ListenAndServe(":8080", nil)
}
你可以使用第三方的包来处理基于JSON的RESTAPI, 它为JSON API Server(JAS). JAS是建立在HTTP之上,它会自动的处理请求到资源。 https://github.com/coocood/jas.
Concurrent WebSockets
在之前的章节提到过,一个WebSocket是保持客户端和服务器连接的一种方法,典型的应用是,替换浏览器和服务器之间的,多个http连接。
使用WebSockets建立API,可以减少客户端和服务器的延迟(每次TCP都需要三次握手),减少长轮询应用的复杂度.
为了说明它的优点,我们对比两张图,第一张是标准的HTTP请求
现在我们对比下WebSocket, 它也是建立在TCP之上,但它减少了多个handshake的开销和状态的控制
以下是一个WebSocket的例子,它接收一个消息,然后返回这个消息的长度
package main
import (
"fmt"
"net/http"
"code.google.com/p/go.net/websocket"
"strconv"
)
var addr = ":12345"
func EchoLengthServer(ws *websocket.Conn) {
var msg string
for {
websocket.Message.Receive(ws, &msg)
fmt.Println("Got message",msg)
length := len(msg)
if err := websocket.Message.Send(ws, strconv.FormatInt(int64(length),
10) ) ; err != nil {
fmt.Println("Can't send message length")
break
}
}
注意这里的循环,你的WebSocket只能是在客户端断开。
func websocketListen() {
http.Handle("/length", websocket.Handler(EchoLengthServer))
err := http.ListenAndServe(addr, nil)
if err != nil {
panic("ListenAndServe: " + err.Error())
}
}
这是socket的主路由,我们将在端口12345上监听,并且计算消息的长度,并且返回。本质上讲,我们将http处理器映射到websocket处理器上。代码如下
func main() {
http.HandleFunc("/websocket", func(w http.ResponseWriter, r
*http.Request) {
http.ServeFile(w, r, "websocket.html")
})
websocketListen()
}
更多的你可以查看https://code.google.com/p/go/source/browse/websocket/websocket.go?repo=net.
分离我们的API逻辑
如我们在之前的版本章节中提到,为了跨不同的版本和格式,我们需要将API的逻辑进行分离。
我们之前看到过GetFormat()和SetFormat()函数,它们独立于所有的端点和版本。
扩展我们的error message
在上一章,我们简单的接触了通过HTTP状态码发送错误消息。我们使用409状态,表示在创建一个已存在于数据库的邮件地址冲突。
http包中的状态码并不能完全的表示REST中的消息,以下是RPC2616的建议
Error | Number |
---|---|
StatusContinue | 100 |
StatusSwitchingProtocols | 101 |
StatusOK | 200 |
StatusCreated | 201 |
StatusAccepted | 202 |
StatusNonAuthoritativeInfo | 203 |
StatusNoContent | 204 |
StatusResetContent | 205 |
StatusPartialContent | 206 |
StatusMultipleChoices | 300 |
StatusMovedPermanently | 301 |
StatusFound | 302 |
StatusSeeOther | 303 |
StatusNotModified | 304 |
StatusUseProxy | 305 |
StatusTemporaryRedirect | 307 |
StatusBadRequest | 400 |
StatusUnauthorized | 401 |
StatusPaymentRequired | 402 |
StatusForbidden | 403 |
StatusNotFound | 404 |
StatusMethodNotAllowed | 405 |
StatusNotAcceptable | 406 |
StatusProxyAuthRequired | 407 |
StatusRequestTimeout | 408 |
StatusConflict | 409 |
StatusGone | 410 |
StatusLengthRequired | 411 |
StatusPreconditionFailed | 412 |
StatusRequestEntityTooLarge | 413 |
StatusRequestURITooLong | 414 |
StatusUnsupportedMediaType | 415 |
StatusRequestedRangeNotSatisfiable | 416 |
StatusExpectationFailed | 417 |
StatusTeapot | 418 |
StatusInternalServerError | 500 |
StatusNotImplemented | 501 |
StatusBadGateway | 502 |
StatusServiceUnavailable | 503 |
StatusGatewayTimeout | 504 |
StatusHTTPVersionNotSupported | 505 |
你可能会回想起,我们之前确编码了这些错误信息。在api.go文件中,我们的ErrorMessage函数中,就明确的定义了,409 HTTP状态码错误。
func ErrorMessages(err int64) (int, int, string) {
errorMessage := ""
statusCode := 200;
errorCode := 0
switch (err) {
case 1062:
errorMessage = http.StatusText(409)
errorCode = 10
statusCode = http.StatusConflict
}
return errorCode, statusCode, errorMessage
}
在这里我们知道,1062是mysql错误,我们将它翻译成http错误,但我们还是需要保持原来的http错误信息,所以需要在switch中增加一个default.
default:
errorMessage = http.StatusText(err)
errorCode = 0
statusCode = err
通过web service更新用户
为了更新用户,我们增加以了一个/api/users/XXX的端点。
Routes.HandleFunc("/api/users/{id:[0-9]+}", UsersUpdate).Methods("PUT")
在UserUpdate函数中,我们首先确认用户的ID是否存在,如果它不存在,我们将返回一个404错误(a document not found error).它是最接近于一个资源记录没有找到。
如果用户存在,我们将通过查询,更新它们的e-mail ID,如果更新失败,我们将返回一个conflict message(或者其它的错误), 如果成功,我们返回200和JSON格式的一个成功的消息。以下是UserUpdates函数
func UsersUpdate(w http.ResponseWriter, r *http.Request) {
Response := UpdateResponse{}
params := mux.Vars(r)
uid := params["id"]
email := r.FormValue("email")
var userCount int
err := Database.QueryRow("SELECT COUNT(user_id) FROM users WHERE
user_id=?", uid).Scan(&userCount)
if userCount == 0 {
error, httpCode, msg := ErrorMessages(404)
log.Println(error)
log.Println(w, msg, httpCode)
Response.Error = msg
Response.ErrorCode = httpCode
http.Error(w, msg, httpCode)
}else if err != nil {
log.Println(error)
} else {
_,uperr := Database.Exec("UPDATE users SET user_email=?WHERE
user_id=?",email,uid)
if uperr != nil {
_, errorCode := dbErrorParse( uperr.Error() )
_, httpCode, msg := ErrorMessages(errorCode)
Response.Error = msg
Response.ErrorCode = httpCode
http.Error(w, msg, httpCode)
} else {
Response.Error = "success"
Response.ErrorCode = 0
output := SetFormat(Response)
fmt.Fprintln(w,string(output))
}
我们将在之后在解释,现在,我们可以创建一个用户,返回多个用户例表和更新用户的e-mail地址
我们在这里介绍两个浏览器端的工具,一个是Postman, 一个是Poster, 它们让你可以直接在浏览器中处理REST端点。可以Google Postman inn Chrome和Poster in Firefox