You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

675 lines
30 KiB
Markdown

2 years ago
# 34 | SDK 设计IAM项目Go SDK设计和实现
你好,我是孔令飞。
上一讲我介绍了公有云厂商普遍采用的SDK设计方式。其实还有一些比较优秀的SDK设计方式比如 Kubernetes的 [client-go](https://github.com/kubernetes/client-go) SDK设计方式。IAM项目参考client-go也实现了client-go风格的SDK[marmotedu-sdk-go](https://github.com/marmotedu/marmotedu-sdk-go)。
和 [33讲](https://time.geekbang.org/column/article/406389) 介绍的SDK设计方式相比client-go风格的SDK具有以下优点
* 大量使用了Go interface特性将接口的定义和实现解耦可以支持多种实现方式。
* 接口调用层级跟资源的层级相匹配,调用方式更加友好。
* 多版本共存。
所以我更推荐你使用marmotedu-sdk-go。接下来我们就来看下marmotedu-sdk-go是如何设计和实现的。
## marmotedu-sdk-go设计
和medu-sdk-go相比marmotedu-sdk-go的设计和实现要复杂一些但功能更强大使用体验也更好。
这里我们先来看一个使用SDK调用iam-authz-server `/v1/authz` 接口的示例,代码保存在 [marmotedu-sdk-go/examples/authz\_clientset/main.go](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.3/examples/authz_clientset/main.go)文件中:
```go
package main
import (
"context"
"flag"
"fmt"
"path/filepath"
"github.com/ory/ladon"
metav1 "github.com/marmotedu/component-base/pkg/meta/v1"
"github.com/marmotedu/component-base/pkg/util/homedir"
"github.com/marmotedu/marmotedu-sdk-go/marmotedu"
"github.com/marmotedu/marmotedu-sdk-go/tools/clientcmd"
)
func main() {
var iamconfig *string
if home := homedir.HomeDir(); home != "" {
iamconfig = flag.String(
"iamconfig",
filepath.Join(home, ".iam", "config"),
"(optional) absolute path to the iamconfig file",
)
} else {
iamconfig = flag.String("iamconfig", "", "absolute path to the iamconfig file")
}
flag.Parse()
// use the current context in iamconfig
config, err := clientcmd.BuildConfigFromFlags("", *iamconfig)
if err != nil {
panic(err.Error())
}
// create the clientset
clientset, err := marmotedu.NewForConfig(config)
if err != nil {
panic(err.Error())
}
request := &ladon.Request{
Resource: "resources:articles:ladon-introduction",
Action:   "delete",
Subject:  "users:peter",
Context: ladon.Context{
"remoteIP": "192.168.0.5",
},
}
// Authorize the request
fmt.Println("Authorize request...")
ret, err := clientset.Iam().AuthzV1().Authz().Authorize(context.TODO(), request, metav1.AuthorizeOptions{})
if err != nil {
panic(err.Error())
}
fmt.Printf("Authorize response: %s.\n", ret.ToString())
}
```
在上面的代码示例中,包含了下面的操作。
* 首先,调用 `BuildConfigFromFlags` 函数创建出SDK的配置实例config
* 接着,调用 `marmotedu.NewForConfig(config)` 创建了IAM项目的客户端 `clientset` ;
* 最后,调用以下代码请求 `/v1/authz` 接口执行资源授权请求:
```go
ret, err := clientset.Iam().AuthzV1().Authz().Authorize(context.TODO(), request, metav1.AuthorizeOptions{})    
if err != nil {           
panic(err.Error())    
}    
fmt.Printf("Authorize response: %s.\n", ret.ToString())
```
调用格式为`项目客户端.应用客户端.服务客户端.资源名.接口` 。
所以上面的代码通过创建项目级别的客户端、应用级别的客户端和服务级别的客户端来调用资源的API接口。接下来我们来看下如何创建这些客户端。
### marmotedu-sdk-go客户端设计
在讲客户端创建之前,我们先来看下客户端的设计思路。
Go项目的组织方式是有层级的**Project -> Application -> Service**。marmotedu-sdk-go很好地体现了这种层级关系使得SDK的调用更加易懂、易用。marmotedu-sdk-go的层级关系如下图所示
![](https://static001.geekbang.org/resource/image/3a/21/3a4721afa7fe365c0954019087d82021.jpg?wh=2248x1043)
marmotedu-sdk-go定义了3类接口分别代表了项目、应用和服务级别的API接口
```go
// 项目级别的接口
type Interface interface {
Iam() iam.IamInterface
Tms() tms.TmsInterface
}
// 应用级别的接口
type IamInterface interface {
APIV1() apiv1.APIV1Interface
AuthzV1() authzv1.AuthzV1Interface
}
// 服务级别的接口
type APIV1Interface interface {
RESTClient() rest.Interface
SecretsGetter
UsersGetter
PoliciesGetter
}
// 资源级别的客户端
type SecretsGetter interface {
Secrets() SecretInterface
}
// 资源的接口定义
type SecretInterface interface {
Create(ctx context.Context, secret *v1.Secret, opts metav1.CreateOptions) (*v1.Secret, error)
Update(ctx context.Context, secret *v1.Secret, opts metav1.UpdateOptions) (*v1.Secret, error)
Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Secret, error)
List(ctx context.Context, opts metav1.ListOptions) (*v1.SecretList, error)
SecretExpansion
}
```
`Interface` 代表了项目级别的接口,里面包含了 `Iam``Tms` 两个应用; `IamInterface` 代表了应用级别的接口里面包含了apiiam-apiserver和authziam-authz-server两个服务级别的接口。api和authz服务中又包含了各自服务中REST资源的CURD接口。
marmotedu-sdk-go通过 `XxxV1` 这种命名方式来支持不同版本的API接口好处是可以在程序中同时调用同一个API接口的不同版本例如
`clientset.Iam().AuthzV1().Authz().Authorize()` 、`clientset.Iam().AuthzV2().Authz().Authorize()` 分别调用了 `/v1/authz``/v2/authz` 两个版本的API接口。
上述关系也可以从目录结构中反映出来marmotedu-sdk-go目录设计如下只列出了一些重要的文件
```bash
├── examples # 存放SDK的使用示例
├── Makefile # 管理SDK源码静态代码检查、代码格式化、测试、添加版权信息等
├── marmotedu
│ ├── clientset.go # clientset实现clientset中包含多个应用多个服务的API接口
│ ├── fake # clientset的fake实现主要用于单元测试
│ └── service # 按应用进行分类存放应用中各服务API接口的具体实现
│ ├── iam # iam应用的API接口实现包含多个服务
│ │ ├── apiserver # iam应用中apiserver服务的API接口包含多个版本
│ │ │ └── v1 # apiserver v1版本API接口
│ │ ├── authz # iam应用中authz服务的API接口
│ │ │ └── v1 # authz服务v1版本接口
│ │ └── iam_client.go # iam应用的客户端包含了apiserver和authz 2个服务的客户端
│ └── tms # tms应用的API接口实现
├── pkg # 存放一些共享包,可对外暴露
├── rest # HTTP请求的底层实现
├── third_party # 存放修改过的第三方包例如gorequest
└── tools
└── clientcmd # 一些函数用来帮助创建rest.Config配置
```
每种类型的客户端,都可以通过以下相似的方式来创建:
```go
config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
clientset, err := xxxx.NewForConfig(config)
```
`/root/.iam/config` 为配置文件,里面包含了服务的地址和认证信息。`BuildConfigFromFlags` 函数加载配置文件,创建并返回 `rest.Config` 类型的配置变量,并通过 `xxxx.NewForConfig` 函数创建需要的客户端。`xxxx` 是所在层级的client包例如 iam、tms。
marmotedu-sdk-go客户端定义了3类接口这可以带来两个好处。
第一API接口调用格式规范层次清晰可以使API接口调用更加清晰易记。
第二可以根据需要自行选择客户端类型调用灵活。举个例子在A服务中需要同时用到iam-apiserver 和 iam-authz-server提供的接口就可以创建应用级别的客户端IamClient然后通过 `iamclient.APIV1()``iamclient.AuthzV1()` 来切换调用不同服务的API接口。
接下来,我们来看看如何创建三个不同级别的客户端。
### 项目级别客户端创建
`Interface` 对应的客户端实现为[Clientset](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/marmotedu/clientset.go#L20-L23),所在的包为 [marmotedu-sdk-go/marmotedu](https://github.com/marmotedu/marmotedu-sdk-go/tree/v1.0.2/marmotedu)Clientset客户端的创建方式为
```go
config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
clientset, err := marmotedu.NewForConfig(config)
```
调用方式为 `clientset.应用.服务.资源名.接口` ,例如:
```go
rsp, err := clientset.Iam().AuthzV1().Authz().Authorize()
```
参考示例为 [marmotedu-sdk-go/examples/authz\_clientset/main.go](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.3/examples/authz_clientset/main.go)。
### 应用级别客户端创建
`IamInterface` 对应的客户端实现为[IamClient](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/marmotedu/service/iam/iam_client.go#L22-L25),所在的包为 [marmotedu-sdk-go/marmotedu/service/iam](https://github.com/marmotedu/marmotedu-sdk-go/tree/v1.0.2/marmotedu/service/iam)IamClient客户端的创建方式为
```go
config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
iamclient,, err := iam.NewForConfig(config)
```
调用方式为 `iamclient.服务.资源名.接口` ,例如:
```go
rsp, err := iamclient.AuthzV1().Authz().Authorize()
```
参考示例为 [marmotedu-sdk-go/examples/authz\_iam/main.go](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/examples/authz_iam/main.go)。
### 服务级别客户端创建
`AuthzV1Interface` 对应的客户端实现为[AuthzV1Client](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/marmotedu/service/iam/authz/v1/authz_client.go#L21-L23),所在的包为 [marmotedu-sdk-go/marmotedu/service/iam/authz/v1](https://github.com/marmotedu/marmotedu-sdk-go/tree/v1.0.2/marmotedu/service/iam/authz/v1)AuthzV1Client客户端的创建方式为
```go
config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
client, err := v1.NewForConfig(config)
```
调用方式为 `client.资源名.接口` ,例如:
```go
rsp, err := client.Authz().Authorize()
```
参考示例为 [marmotedu-sdk-go/examples/authz/main.go](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.3/examples/authz/main.go)。
上面我介绍了marmotedu-sdk-go的客户端创建方法接下来我们再来看下这些客户端具体是如何执行REST API请求的。
## marmotedu-sdk-go的实现
marmotedu-sdk-go的实现和medu-sdk-go一样也是采用分层结构分为API层和基础层。如下图所示
![](https://static001.geekbang.org/resource/image/c4/b2/c40439c97998a01758923394116c33b2.jpg?wh=2248x2097)
[RESTClient](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/rest/client.go#L95-L105)是整个SDK的核心RESTClient向下通过调用[Request](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/rest/request.go#L28-L50)模块来完成HTTP请求方法、请求路径、请求体、认证信息的构建。Request模块最终通过调用[gorequest](https://github.com/parnurzeal/gorequest)包提供的方法完成HTTP的POST、PUT、GET、DELETE等请求获取HTTP返回结果并解析到指定的结构体中。RESTClient向上提供 `Post()``Put()``Get()``Delete()` 等方法来供客户端完成HTTP请求。
marmotedu-sdk-go提供了两类客户端分别是RESTClient客户端和基于RESTClient封装的客户端。
* RESTClientRaw类型的客户端可以通过指定HTTP的请求方法、请求路径、请求参数等信息直接发送HTTP请求例如 `client.Get().AbsPath("/version").Do().Into()`
* 基于RESTClient封装的客户端例如AuthzV1Client、APIV1Client等执行特定REST资源、特定API接口的请求方便开发者调用。
接下来我们具体看下如何创建RESTClient客户端以及Request模块的实现。
### RESTClient客户端实现
我通过下面两个步骤实现了RESTClient客户端。
**第一步,创建**[rest.Config](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/rest/config.go#L29-L60)**类型的变量。**
[BuildConfigFromFlags](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/tools/clientcmd/client_config.go#L190-L203)函数通过加载yaml格式的配置文件来创建 `rest.Config` 类型的变量加载的yaml格式配置文件内容为
```yaml
apiVersion: v1
user:
#token: # JWT Token
username: admin # iam 用户名
password: Admin@2020 # iam 密码
#secret-id: # 密钥 ID
#secret-key: # 密钥 Key
client-certificate: /home/colin/.iam/cert/admin.pem # 用于 TLS 的客户端证书文件路径
client-key: /home/colin/.iam/cert/admin-key.pem # 用于 TLS 的客户端 key 文件路径
#client-certificate-data:
#client-key-data:
server:
address: https://127.0.0.1:8443 # iam api-server 地址
timeout: 10s # 请求 api-server 超时时间
#max-retries: # 最大重试次数,默认为 0
#retry-interval: # 重试间隔,默认为 1s
#tls-server-name: # TLS 服务器名称
#insecure-skip-tls-verify: # 设置为 true 表示跳过 TLS 安全验证模式,将使得 HTTPS 连接不安全
certificate-authority: /home/colin/.iam/cert/ca.pem # 用于 CA 授权的 cert 文件路径
#certificate-authority-data:
```
在配置文件中,我们可以指定服务的地址、用户名/密码、密钥、TLS证书、超时时间、重试次数等信息。
创建方法如下:
```go
config, err := clientcmd.BuildConfigFromFlags("", *iamconfig)    
if err != nil {                                                  
    panic(err.Error())    
}  
```
这里的代码中,`*iamconfig` 是yaml格式的配置文件路径。`BuildConfigFromFlags` 函数中,调用[LoadFromFile](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.3/tools/clientcmd/loader.go#L32-L56)函数来解析yaml配置文件。LoadFromFile最终是通过 `yaml.Unmarshal` 的方式来解析yaml格式的配置文件的。
**第二步根据rest.Config类型的变量创建RESTClient客户端。**
通过[RESTClientFor](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/rest/config.go#L191-L237)函数来创建RESTClient客户端
```go
func RESTClientFor(config *Config) (*RESTClient, error) {
...
baseURL, versionedAPIPath, err := defaultServerURLFor(config)
if err != nil {
return nil, err
}
// Get the TLS options for this client config
tlsConfig, err := TLSConfigFor(config)
if err != nil {
return nil, err
}
// Only retry when get a server side error.
client := gorequest.New().TLSClientConfig(tlsConfig).Timeout(config.Timeout).
Retry(config.MaxRetries, config.RetryInterval, http.StatusInternalServerError)
// NOTICE: must set DoNotClearSuperAgent to true, or the client will clean header befor http.Do
client.DoNotClearSuperAgent = true
...
clientContent := ClientContentConfig{
Username: config.Username,
Password: config.Password,
SecretID: config.SecretID,
SecretKey: config.SecretKey,
...
}
return NewRESTClient(baseURL, versionedAPIPath, clientContent, client)
}
```
RESTClientFor函数调用[defaultServerURLFor(config)](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/rest/url_utils.go#L69-L81)生成基本的HTTP请求路径baseURL=http://127.0.0.1:8080versionedAPIPath=/v1。然后通过[TLSConfigFor](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/rest/config.go#L241-L298)函数生成TLS配置并调用 `gorequest.New()` 创建gorequest客户端将客户端配置信息保存在变量中。最后调用[NewRESTClient](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/rest/client.go#L109-L130)函数创建RESTClient客户端。
RESTClient客户端提供了以下方法来供调用者完成HTTP请求
```go
func (c *RESTClient) APIVersion() scheme.GroupVersion
func (c *RESTClient) Delete() *Request
func (c *RESTClient) Get() *Request
func (c *RESTClient) Post() *Request
func (c *RESTClient) Put() *Request
func (c *RESTClient) Verb(verb string) *Request
```
可以看到RESTClient提供了 `Delete``Get``Post``Put` 方法分别用来执行HTTP的DELETE、GET、POST、PUT方法提供的 `Verb` 方法可以灵活地指定HTTP方法。这些方法都返回了 `Request` 类型的变量。`Request` 类型的变量提供了一些方法用来完成具体的HTTP请求例如
```go
type Response struct {
    Allowed bool   `json:"allowed"`
    Denied  bool   `json:"denied,omitempty"`
    Reason  string `json:"reason,omitempty"`
    Error   string `json:"error,omitempty"`
}
func (c *authz) Authorize(ctx context.Context, request *ladon.Request, opts metav1.AuthorizeOptions) (result *Response, err error) {
    result = &Response{}                                         
    err = c.client.Post().
        Resource("authz").
        VersionedParams(opts).
        Body(request).
        Do(ctx).
        Into(result)
    return
}
```
上面的代码中, `c.client` 是RESTClient客户端通过调用RESTClient客户端的 `Post` 方法,返回了 `*Request` 类型的变量。
`*Request` 类型的变量提供了 `Resource``VersionedParams` 方法来构建请求HTTP URL中的路径 `/v1/authz` ;通过 `Body` 方法指定了HTTP请求的Body。
到这里我们分别构建了HTTP请求需要的参数HTTP Method、请求URL、请求Body。所以之后就可以调用 `Do` 方法来执行HTTP请求并将返回结果通过 `Into` 方法保存在传入的result变量中。
### Request模块实现
RESTClient客户端的方法会返回Request类型的变量Request类型的变量提供了一系列的方法用来构建HTTP请求参数并执行HTTP请求。
所以Request模块可以理解为最底层的通信层我们来看下Request模块具体是如何完成HTTP请求的。
我们先来看下[Request结构体](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.3/rest/request.go#L28-L50)的定义:
```go
type RESTClient struct {           
    // base is the root URL for all invocations of the client    
    base *url.URL    
    // group stand for the client group, eg: iam.api, iam.authz                       
    group string                                                                          
    // versionedAPIPath is a path segment connecting the base URL to the resource root    
    versionedAPIPath string                                      
    // content describes how a RESTClient encodes and decodes responses.    
    content ClientContentConfig    
    Client  *gorequest.SuperAgent    
}
type Request struct {
c *RESTClient
timeout time.Duration
// generic components accessible via method setters
verb       string
pathPrefix string
subpath    string
params     url.Values
headers    http.Header
// structural elements of the request that are part of the IAM API conventions
// namespace    string
// namespaceSet bool
resource     string
resourceName string
subresource  string
// output
err  error
body interface{}
}  
```
再来看下Request结构体提供的方法
```go
func (r *Request) AbsPath(segments ...string) *Request
func (r *Request) Body(obj interface{}) *Request
func (r *Request) Do(ctx context.Context) Result
func (r *Request) Name(resourceName string) *Request
func (r *Request) Param(paramName, s string) *Request
func (r *Request) Prefix(segments ...string) *Request
func (r *Request) RequestURI(uri string) *Request
func (r *Request) Resource(resource string) *Request
func (r *Request) SetHeader(key string, values ...string) *Request
func (r *Request) SubResource(subresources ...string) *Request
func (r *Request) Suffix(segments ...string) *Request
func (r *Request) Timeout(d time.Duration) *Request
func (r *Request) URL() *url.URL
func (r *Request) Verb(verb string) *Request
func (r *Request) VersionedParams(v interface{}) *Request
```
通过Request结构体的定义和使用方法我们不难猜测出Request模块通过 `Name``Resource``Body``SetHeader` 等方法来设置Request结构体中的各个字段。这些字段最终用来构建出一个HTTP请求并通过 `Do` 方法来执行HTTP请求。
那么如何构建并执行一个HTTP请求呢我们可以通过以下5步来构建并执行HTTP请求
1. 构建HTTP URL
2. 构建HTTP Method
3. 构建HTTP Body
4. 执行HTTP请求
5. 保存HTTP返回结果。
接下来我们就来具体看下Request模块是如何构建这些请求参数并发送HTTP请求的。
**第一步构建HTTP URL。**
首先,通过[defaultServerURLFor](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.3/rest/url_utils.go#L69-L81)函数返回了`http://iam.api.marmotedu.com:8080` 和 `/v1` 并将二者分别保存在了Request类型结构体变量中 `c` 字段的 `base` 字段和 `versionedAPIPath` 字段中。
通过 [Do](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.3/rest/request.go#L379-L416) 方法执行HTTP时会调用[r.URL()](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.3/rest/request.go#L392)方法来构建请求URL。 `r.URL` 方法中通过以下代码段构建了HTTP请求URL
```go
func (r *Request) URL() *url.URL {
    p := r.pathPrefix
    if len(r.resource) != 0 {
        p = path.Join(p, strings.ToLower(r.resource))
    }
    if len(r.resourceName) != 0 || len(r.subpath) != 0 || len(r.subresource) != 0 {
        p = path.Join(p, r.resourceName, r.subresource, r.subpath)
    }
                                                                                   
    finalURL := &url.URL{}
    if r.c.base != nil {
        *finalURL = *r.c.bas
    }
 
    finalURL.Path = p
...    
}
```
`p := r.pathPrefix``r.c.base` ,是通过 `defaultServerURLFor` 调用返回的 `v1``http://iam.api.marmotedu.com:8080` 来构建的。
`resourceName` 通过 `func (r *Request) Resource(resource string) *Request` 来指定,例如 `authz`
所以最终我们构建的请求URL为 `http://iam.api.marmotedu.com:8080/v1/authz`
**第二步构建HTTP Method。**
HTTP Method通过RESTClient提供的 `Post` 、`Delete` 、`Get` 等方法来设置,例如:
```go
func (c *RESTClient) Post() *Request {                                                                                 
    return c.Verb("POST")                                                                                              
}
func (c *RESTClient) Verb(verb string) *Request {                                                                      
    return NewRequest(c).Verb(verb)                                                                                    
}
```
`NewRequest(c).Verb(verb)` 最终设置了Request结构体的 `verb` 字段,供 `Do` 方法使用。
**第三步构建HTTP Body。**
HTTP Body通过Request结构体提供的Body方法来指定
```go
func (r *Request) Body(obj interface{}) *Request {                    
    if v := reflect.ValueOf(obj); v.Kind() == reflect.Struct {              
        r.SetHeader("Content-Type", r.c.content.ContentType)                
    }                                                                                                                  
                                                                                                                       
    r.body = obj                                                                                                       
                                                                                                                       
    return r                                                                                                           
} 
```
**第四步执行HTTP请求。**
通过Request结构体提供的Do方法来执行具体的HTTP请求代码如下
```go
func (r *Request) Do(ctx context.Context) Result {
client := r.c.Client
client.Header = r.headers
if r.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, r.timeout)
defer cancel()
}
client.WithContext(ctx)
resp, body, errs := client.CustomMethod(r.verb, r.URL().String()).Send(r.body).EndBytes()
if err := combineErr(resp, body, errs); err != nil {
return Result{
response: &resp,
err:      err,
body:     body,
}
}
decoder, err := r.c.content.Negotiator.Decoder()
if err != nil {
return Result{
response: &resp,
err:      err,
body:     body,
decoder:  decoder,
}
}
return Result{
response: &resp,
body:     body,
decoder:  decoder,
}
}
```
在Do方法中使用了Request结构体变量中各个字段的值通过 `client.CustomMethod` 来执行HTTP请求。 `client``*gorequest.SuperAgent` 类型的客户端。
**第五步保存HTTP返回结果。**
通过Request结构体的 `Into` 方法来保存HTTP返回结果
```go
func (r Result) Into(v interface{}) error {
    if r.err != nil {                                          
        return r.Error()
    }                                                                                 
                                                         
    if r.decoder == nil {                                                                    
        return fmt.Errorf("serializer doesn't exist")
    }                            
                             
    if err := r.decoder.Decode(r.body, &v); err != nil {
        return err                                                                    
    }                                                                                        
                                                             
    return nil                                                                      
}
```
`r.body` 是在Do方法中执行完HTTP请求后设置的它的值为HTTP请求返回的Body。
### 请求认证
接下来我再来介绍下marmotedu-sdk-go另外一个比较核心的功能请求认证。
marmotedu-sdk-go支持两种认证方式
* Basic认证通过给请求添加 `Authorization: Basic xxxx` 来实现。
* Bearer认证通过给请求添加 `Authorization: Bearer xxxx` 来实现。这种方式又支持直接指定JWT Token或者通过指定密钥对由SDK自动生成JWT Token。
Basic认证和Bearer认证我在 [25讲](https://time.geekbang.org/column/article/398410)介绍过,你可以返回查看下。
认证头是RESTClient客户端发送HTTP请求时指定的具体实现位于[NewRequest](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/rest/request.go#L53-L102)函数中:
```go
switch {
case c.content.HasTokenAuth():
r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", c.content.BearerToken))
case c.content.HasKeyAuth():
tokenString := auth.Sign(c.content.SecretID, c.content.SecretKey, "marmotedu-sdk-go", c.group+".marmotedu.com")
r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", tokenString))
case c.content.HasBasicAuth():
// TODO: get token and set header
r.SetHeader("Authorization", "Basic "+basicAuth(c.content.Username, c.content.Password))
}
```
上面的代码会根据配置信息,自动判断使用哪种认证方式。
## 总结
这一讲中我介绍了Kubernetes client-go风格的SDK实现方式。和公有云厂商的SDK设计相比client-go风格的SDK设计有很多优点。
marmotedu-sdk-go在设计时通过接口实现了3类客户端分别是项目级别的客户端、应用级别的客户端和服务级别的客户端。开发人员可以根据需要自行创建客户端类型。
marmotedu-sdk-go通过[RESTClientFor](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/rest/config.go#L191-L237)创建了RESTClient类型的客户端RESTClient向下通过调用[Request](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/rest/request.go#L28-L50)模块来完成HTTP请求方法、请求路径、请求体、认证信息的构建。Request模块最终通过调用[gorequest](https://github.com/parnurzeal/gorequest)包提供的方法完成HTTP的POST、PUT、GET、DELETE等请求获取HTTP返回结果并解析到指定的结构体中。RESTClient向上提供 `Post()``Put()``Get()``Delete()` 等方法来供客户端完成HTTP请求。
## 课后练习
1. 阅读[defaultServerURLFor](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.3/rest/url_utils.go#L69-L81)源码思考下defaultServerURLFor是如何构建请求地址 `http://iam.api.marmotedu.com:8080` 和API版本 `/v1` 的。
2. 使用[gorequest](https://github.com/parnurzeal/gorequest)包编写一个可以执行以下HTTP请求的示例
```bash
curl -XPOST http://example.com/v1/user -d '{"username":"colin","address":"shenzhen"}'
```
欢迎你在留言区与我交流讨论,我们下一讲见。