精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Go 開發者必修課:如何優雅的設計和實現 API 接口的錯誤返回

開發 后端
本節課探討了錯誤返回的優秀實踐,比較了兩種常見的錯誤返回方式,并選擇了更符合企業級開發需求的第二種方式。

在 Go 項目開發中,有很多基礎的 Go 包需要我們去設計。其中,錯誤包是 Go 項目開發必須要考慮的一個設計。

錯誤包在 Go 項目開發中主要用來返回錯誤或者打印錯誤。返回錯誤時,既需在代碼內返回錯誤,又需要將錯誤返回給用戶。在設計和實現錯誤包的時候,需要考慮上述使用場景。

一、錯誤返回方法

在 Go 項目開發中,錯誤的返回方式通常有以下兩種:

  • 始終返回 HTTP 200 狀態碼,并在 HTTP 返回體中返回錯誤信息;
  • 返回 HTTP 400 狀態碼(Bad Request),并在 HTTP 返回體中返回錯誤信息。

方式一:成功返回,返回體中返回錯誤信息

例如 Facebook API 的錯誤返回設計,始終返回 200 HTTP 狀態碼:

{
  "error": {
    "message": "Syntax error \"Field picture specified more than once. This is only possible before version 2.1\" at character 23: id,name,picture,picture",
    "type": "OAuthException",
    "code": 2500,
    "fbtrace_id": "xxxxxxxxxxx"
  }
}

在上述錯誤返回的實現方式中,HTTP 狀態碼始終固定返回 200,僅需關注業務錯誤碼,整體實現較為簡單。然而,此方式存在一個明顯的缺點:對于每一次 HTTP 請求,既需要檢查 HTTP 狀態碼以判斷請求是否成功,還需要解析響應體以獲取業務錯誤碼,從而判斷業務邏輯是否成功。理想情況下,我們期望客戶端對成功的 HTTP 請求能夠直接將響應體解析為需要的 Go 結構體,并進行后續的業務邏輯處理,而不用再判斷請求是否成功。

方式二:失敗返回,返回體中返回錯誤信息

Twitter API 的錯誤返回設計會根據錯誤類型返回對應的 HTTP 狀態碼,并在返回體中返回錯誤信息和自定義業務錯誤碼。成功的業務請求則返回 200 HTTP 狀態碼。例如:

HTTP/1.1 400 Bad Request
x-connection-hash: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
set-cookie: guest_id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Date: Thu, 01 Jun 201703:04:23 GMT
Content-Length: 62
x-response-time: 5
strict-transport-security: max-age=631138519
Connection: keep-alive
Content-Type: application/json; charset=utf-8
Server: tsa_b

{
"errors": [
    {
      "code": 215,
      "message": "Bad Authentication data."
    }
  ]
}

方式二相比方式一,對于成功的請求不需要再次判錯。然而,方式二還可以進一步優化:整數格式的業務錯誤碼 215 可讀性較差,用戶無法從 215 直接獲取任何有意義的信息。建議將其替換為語義化的字符串,例如:NotFound.PostNotFound。

Twitter API 返回的錯誤是一個數組,在實際開發獲取錯誤時,需要先判斷數組是否為空,如不為空,再從數組中獲取錯誤,開發復雜度較高。建議采用更簡單的錯誤返回格式:

{
  "code": "InvalidParameter.BadAuthenticationData",
  "message": "Bad Authentication data."
}

需要特別注意的是,message 字段會直接展示給外部用戶,因此必須確保其內容不包含敏感信息,例如數據庫的 id 字段、內部組件的 IP 地址、用戶名等信息。返回的錯誤信息中,還可以根據需要返回更多字段,例如:錯誤指引文檔 URL 等。

二、miniblog 錯誤返回設計和實現

miniblog 項目錯誤返回格式采用了方式二,在接口失敗時返回對應的 HTTP/gRPC 狀態碼,并在返回體中返回具體的錯誤信息,例如:

HTTP/1.1 404 Not Found
...
{
  "code": "NotFound.UserNotFound",
  "message": "User not found."
}

在錯誤返回方式二中,需要返回一個業務錯誤碼。返回業務錯誤碼可以帶來以下好處:

  • 快速定位問題:開發人員可以借助錯誤碼迅速定位問題,并精確到具體的代碼行。例如,錯誤碼可以直接指示問題的含義,同時通過工具(如 grep)輕松定位到錯誤碼在代碼中的具體位置;
  • 便于排查問題:用戶能夠通過錯誤碼判斷接口失敗的原因,并將錯誤碼提供給開發人員,以便快速定位問題并進行排查;
  • 承載豐富信息:錯誤碼通常包含了詳細的信息,例如錯誤的級別、所屬錯誤類別以及具體的錯誤描述。這些錯誤信息可以幫助用戶和開發者快速定位問題;
  • 靈活定義:錯誤碼由開發者根據需要靈活定義,不依賴和受限于第三方框架,例如 net/http 和 google.golang.org/grpc;
  • 便于邏輯判斷:在業務開發中,判斷錯誤類別以執行對應的邏輯處理是一個常見需求。通過自定義錯誤碼,可以輕松實現。例如:
import "errors"
import "path/to/errno"

if errors.Is(err, errno.InternalServerError) {
    // 對應錯誤處理邏輯
}

1. 制定錯誤碼規范

錯誤碼是直接暴露給用戶的,因此需要設計一個易讀、易懂且規范化的錯誤碼。在設計錯誤碼時可以根據實際需求自行設計,也可以參考其他優秀的設計方案。

一般來說,當調研某項技術實現時,建議優先參考各大公有云廠商的實現方式,例如騰訊云、阿里云、華為云等。這些公有云廠商直接面向企業和個人,專注于技術本身,擁有強大的技術團隊,因此它們的設計與實現具有很高的參考價值。

經過調研,此處采用了騰訊云 API 3.0 的錯誤碼設計規范,并將規范文檔保存在項目的文檔目錄中:docs/devel/zh-CN/conversions/error_code.md[2]。

騰訊云采用了兩級錯誤碼設計。以下是兩級錯誤碼設計相較于簡單錯誤碼(如 215、InvalidParameter)的優勢:

  • 語義化: 語義化的錯誤碼可以通過名字直接反映錯誤的類型,便于快速理解錯誤;
  • 更加靈活: 二級錯誤碼的格式為<平臺級.資源級>。其中,平臺級錯誤碼是固定值,用于指代某一類錯誤,客戶端可以利用該錯誤碼進行通用錯誤處理。資源級錯誤碼則用于更精確的錯誤定位。此外,服務端既可根據需求自定義錯誤碼,也可使用默認錯誤碼。

miniblog 項目預定義了一些平臺級錯誤碼,如下表所示。

錯誤碼

錯誤描述

錯誤類型

OK

請求成功

-

InternalError

內部錯誤

1

NotFound

資源不存在

0

BindError

綁定失敗,解析請求體失敗

0

InvalidArgument

參數錯誤(包括參數類型、格式、值等錯誤)

0

Unauthenticated

認證失敗

0

PermissionDenied

授權失敗

0

OperationFailed

操作失敗

2

上表中,錯誤類型 0 代表客戶端錯誤,1 代表服務端錯誤,2 代表客戶端錯誤/服務端錯誤,- 代表請求成功。

2. miniblog 錯誤包設計

開發一個錯誤包,需要先為錯誤包起一個易讀、易理解的包名。在 Go 項目開發中,如果自定義包的名稱如 errors、context 等,會與 Go 標準庫中已存在的 errors 或 context 包發生命名沖突,如果代碼中需要同時使用自定義包與標準庫包時,通常會通過為標準庫包起別名的方式解決。例如,可以通過 import stderrors "errors" 來為標準庫的 errors 包定義別名。

為了避免頻繁使用這種起別名的操作,在開發自定義包時,可以從包命名上避免與標準庫包名沖突。建議將可能沖突的包命名為 <沖突包原始名>x,其名稱中的“x”代表擴展(extended)或實驗(experimental)。這種命名方式是一種擴展命名約定,通常用于表示此包是對標準庫中已有包功能的擴展或補充。需要注意的是,這并非 Go 語言的官方規范,而是開發者為了防止命名沖突、增強語義所采用的命名方式。miniblog 項目的自定義 contextx 包也采用了這種命名風格。

因此,為了避免與標準庫的 errors 包命名沖突,miniblog 項目的錯誤包命名為 errorsx,寓意為“擴展的錯誤處理包”。

由于 miniblog 項目的錯誤包命名為 errorsx,為保持命名一致性,定義了一個名為 ErrorX 的結構體,用于描述錯誤信息,具體定義如下:

// ErrorX 定義了 OneX 項目體系中使用的錯誤類型,用于描述錯誤的詳細信息.
type ErrorX struct {
    // Code 表示錯誤的 HTTP 狀態碼,用于與客戶端進行交互時標識錯誤的類型.
    Code int`json:"code,omitempty"`

    // Reason 表示錯誤發生的原因,通常為業務錯誤碼,用于精準定位問題.
    Reason string`json:"reason,omitempty"`

    // Message 表示簡短的錯誤信息,通常可直接暴露給用戶查看.
    Message string`json:"message,omitempty"`

    // Metadata 用于存儲與該錯誤相關的額外元信息,可以包含上下文或調試信息.
    Metadata map[string]string`json:"metadata,omitempty"`
}

ErrorX 是一個錯誤類型,因此需要實現 Error 方法:

// Error 實現 error 接口中的 `Error` 方法.
func (err *ErrorX) Error() string {
    return fmt.Sprintf("error: code = %d reason = %s message = %s metadata = %v", err.Code, err.Reason, err.Message, err.Metadata)
}

Error() 返回的錯誤信息中,包含了 HTTP 狀態碼、錯誤發生的原因、錯誤信息和額外的錯誤元信息。通過這些詳盡的錯誤信息返回,幫助開發者快速定位錯誤。

提示

miniblog 項目屬于 OneX 技術體系中的一個實戰項目,其設計和實現方式跟 OneX 技術體系中的其他項目保持一致。考慮到包的復用性,errorsx 包的實現位于 onexstack[3] 項目根目錄下的 pkg/errorsx 目錄中。

在 Go 項目開發中,發生錯誤的原因有很多,大多數情況下,開發者希望將真實的錯誤信息返回給用戶。因此,還需要提供一個方法用來設置 ErrorX 結構體中的 Message 字段。同樣的,還需要提供設置 Metadata 字段的方法。為了滿足上述訴求,給 ErrorX 增加 WithMessage、WithMetadata、KV 三個方法。實現方式如下述代碼所示。

// WithMessage 設置錯誤的 Message 字段.
func (err *ErrorX) WithMessage(format string, args ...any) *ErrorX {
    err.Message = fmt.Sprintf(format, args...)
    return err
}

// WithMetadata 設置元數據.
func (err *ErrorX) WithMetadata(md map[string]string) *ErrorX {
    err.Metadata = md
    return err
}

// KV 使用 key-value 對設置元數據.
func (err *ErrorX) KV(kvs ...string) *ErrorX {
    if err.Metadata == nil {
        err.Metadata = make(map[string]string) // 初始化元數據映射
    }

    for i := 0; i < len(kvs); i += 2 {
        // kvs 必須是成對的
        if i+1 < len(kvs) {
            err.Metadata[kvs[i]] = kvs[i+1]
        }
    }
    return err
}

在上述代碼中,設置 Message、Metadata 字段的方法名分別為 WithMessage、WithMetadata。WithXXX,在 Go 項目開發中是一種很常見的命名方式,寓意是:設置 XXX。KV 方法則以追加的方式給 Metadata 增加鍵值對。WithMessage、WithMetadata、KV 都返回了 *ErrorX 類型的實例,目的是為了實現鏈式調用,例如:

err := new(ErrorX)
err.WithMessage("Message").WithMetadata(map[string]string{"key":"value"})

在 Go 項目開發中,鏈式調用(chained method calls)是一種常見的設計模式,該模式通過在方法中返回對象自身,使多個方法調用可以連續進行。鏈式調用的好處在于:簡化代碼、提高可讀性、減少錯誤可能性和增強擴展性,尤其是在對象構造或逐步修改操作時,非常高效直觀。合理使用鏈式調用可以顯著提升代碼的質量和開發效率,同時讓接口設計更加優雅。

errorsx 包的設計目標不僅適用于 HTTP 接口的錯誤返回,還適用于 gRPC 接口的錯誤返回。因此,ErrorX 結構體還實現了 GRPCStatus() 方法。GRPCStatus() 方法的作用是將自定義錯誤類型 ErrorX 轉換為 gRPC 的 status.Status 類型,用于生成 gRPC 標準化的錯誤返回信息(包括錯誤碼、錯誤消息及詳細錯誤信息),從而滿足 gRPC 框架的錯誤處理要求。GRPCStatus() 方法實現如下:

// GRPCStatus 返回 gRPC 狀態表示.
func (err *ErrorX) GRPCStatus() *status.Status {
    details := errdetails.ErrorInfo{Reason: err.Reason, Metadata: err.Metadata}
    s, _ := status.New(httpstatus.ToGRPCCode(err.Code), err.Message).WithDetails(&details)
    return s
}

在 Go 項目開發中,通常需要將一個 error 類型的錯誤 err,解析為 *ErrorX 類型,并獲取 *ErrorX 中的 Code 字段和 Reason 字段的值。Code 字段可用來設置 HTTP 狀態碼,Reason 字段可用來判斷錯誤類型。為此,errorsx 包實現了 FromError、Code、Reason 方法,具體實現如下:

// Code 返回錯誤的 HTTP 代碼.
func Code(err error) int {
    if err == nil {
        return http.StatusOK //nolint:mnd
    }
    return FromError(err).Code
}

// Reason 返回特定錯誤的原因.
func Reason(err error) string {
    if err == nil {
        return ErrInternal.Reason
    }
    return FromError(err).Reason
}

// FromError 嘗試將一個通用的 error 轉換為自定義的 *ErrorX 類型.
func FromError(err error) *ErrorX {
    // 如果傳入的錯誤是 nil,則直接返回 nil,表示沒有錯誤需要處理.
    if err == nil {
        returnnil
    }

    // 檢查傳入的 error 是否已經是 ErrorX 類型的實例.
    // 如果錯誤可以通過 errors.As 轉換為 *ErrorX 類型,則直接返回該實例.
    if errx := new(ErrorX); errors.As(err, &errx) {
        return errx
    }

    // gRPC 的 status.FromError 方法嘗試將 error 轉換為 gRPC 錯誤的 status 對象.
    // 如果 err 不能轉換為 gRPC 錯誤(即不是 gRPC 的 status 錯誤),
    // 則返回一個帶有默認值的 ErrorX,表示是一個未知類型的錯誤.
    gs, ok := status.FromError(err)
    if !ok {
        return New(ErrInternal.Code, ErrInternal.Reason, err.Error())
    }

    // 如果 err 是 gRPC 的錯誤類型,會成功返回一個 gRPC status 對象(gs).
    // 使用 gRPC 狀態中的錯誤代碼和消息創建一個 ErrorX.
    ret := New(httpstatus.FromGRPCCode(gs.Code()), ErrInternal.Reason, gs.Message())

    // 遍歷 gRPC 錯誤詳情中的所有附加信息(Details).
    for _, detail := range gs.Details() {
        if typed, ok := detail.(*errdetails.ErrorInfo); ok {
            ret.Reason = typed.Reason
            return ret.WithMetadata(typed.Metadata)
        }
    }

    return ret
}

在 Go 項目開發中,經常還要對比一個 error 類型的錯誤 err 是否是某個預定義錯誤,因此 *ErrorX 也需要實現一個 Is 方法,Is 方法實現如下:

// Is 判斷當前錯誤是否與目標錯誤匹配.
// 它會遞歸遍歷錯誤鏈,并比較 ErrorX 實例的 Code 和 Reason 字段.
// 如果 Code 和 Reason 均相等,則返回 true;否則返回 false.
func (err *ErrorX) Is(target error) bool {
    if errx := new(ErrorX); errors.As(target, &errx) {
        return errx.Code == err.Code && errx.Reason == err.Reason
    }
    return false
}

Is 方法中,通過對比 Code 和 Reason 字段,來判斷 target 錯誤是否是指定的預定義錯誤。注意,Is 方法中,沒有對比 Message 字段的值,這是因為 Message 字段的值通常是動態的,而錯誤類型的定義不依賴于 Message。

至此,成功為 miniblog 開發了一個滿足項目需求的錯誤包 errorsx,代碼完整實現見 onexstack 項目的 pkg/errorsx/errorsx.go[4] 文件。

3. miniblog 錯誤碼定義

在實現了 errorsx 錯誤包之后,便可以根據需要預定義項目需要的錯誤。這些錯誤,可以在代碼中便捷的引用。通過直接引用預定義錯誤,不僅可以提高開發效率,還可以保持整個項目的錯誤返回是一致的。

miniblog 的預定義錯誤定義在 internal/pkg/errno[5] 目錄下。一些基礎錯誤定義如下:

var (
    // OK 代表請求成功.
    OK = &errorsx.ErrorX{Code: http.StatusOK, Message: ""}

    // ErrInternal 表示所有未知的服務器端錯誤.
    ErrInternal = errorsx.ErrInternal

    ...

    // ErrPageNotFound 表示頁面未找到.
    ErrPageNotFound = &errorsx.ErrorX{Code: http.StatusNotFound, Reason: "NotFound.PageNotFound", Message: "Page not found."}
    ...
)

更完整的預定義錯誤,可直接查看 internal/pkg/errno 中的錯誤定義文件。預定義錯誤保存在 internal/pkg 目錄中,是因為這些錯誤跟 miniblog 項目耦合,不是通用的錯誤定義。

至此,miniblog 成功實現了錯誤返回代碼的實現,完整代碼見分支 feature/s08[6]。

4. miniblog 錯誤返回規范

為了標準化接口錯誤返回,提高接口錯誤返回的易讀性,miniblog 制定了以下錯誤返回規范:

  • 所有接口都要返回 errorsx.ErrorX 類型的錯誤;
  • 建議在錯誤的原始位置,使用 errno.ErrXXX 方式返回 miniblog 自定義錯誤類型,其他位置直接透傳自定義錯誤:
package main

import (
    "github.com/onexstack/miniblog/internal/pkg/errno"
    "github.com/onexstack/miniblog/internal/pkg/log"
)

func main() {
    if err := validateUser(); err != nil {
        panic(err)
    }
}

func validatePassword(password string) error {
    iflen(password) < 6 {
        log.Errorw("Password is too short")
        // 在錯誤最原始位置封裝自定義錯誤
        // 方式1:不帶自定義信息的錯誤返回
        return errno.ErrPasswordInvalid
        // 方式2:帶有自定義信息的錯誤返回
        //return errno.ErrPasswordInvalid.WithMessage("Password is too short")
    }
    returnnil
}

func validateUser() error {
    // 直接透傳 validatePassword 返回的自定義錯誤
    if err := validatePassword("test"); err != nil {
        return err
    }
    returnnil
}

三、minilbog 錯誤包測試

本節就來測試下 errorsx 錯誤包及 errno 錯誤碼。測試代碼保存在 examples/errorsx/main.go[7] 文件中,代碼如下:

package main

import (
    "fmt"

    "github.com/onexstack/onexstack/pkg/errorsx"

    "github.com/onexstack/miniblog/internal/pkg/errno"
)

func main() {
    // 創建了一個 ErrorX 錯誤,表示數據庫連接失敗。
    // Code: 500,表明是服務器內部錯誤。
    // Reason: "InternalError.DBConnection",表示錯誤的具體分類。
    // Message: "Something went wrong: DB connection failed",表示該錯誤的具體信息。
    errx := errorsx.New(500, "InternalError.DBConnection", "Something went wrong: %s", "DB connection failed")

    // fmt.Println 會調用 errx 的 Error 方法,輸出:
    // error: code = 500 reason = InternalError.DBConnection message = Something went wrong: DB connection failed metadata = map[]
    fmt.Println(errx)

    // 給錯誤添加元數據,增強錯誤的上下文信息,便于調試和追蹤。
    errx.WithMetadata(map[string]string{
        "user_id":    "12345",   // 添加用戶 ID 信息
        "request_id": "abc-def", // 添加請求 ID 信息
    })

    // 繼續向錯誤中添加元數據,這次使用了 KV 方法,它是一種更加簡潔的方式,用 key-value 的模式逐一設置元數據。
    // 這里添加 trace_id 信息,用于關聯分布式鏈路信息。
    errx.KV("trace_id", "xyz-789")

    // 使用 WithMessage 方法更新錯誤的 Message 字段。
    // 更新后的 Message 是:Updated message: retry failed。
    // Note: 更新消息字段并不會影響 Code、Reason 和 Metadata,它只是說明錯誤的上下文發生了變化。
    errx.WithMessage("Updated message: %s", "retry failed")

    // 再次打印 errx,此時的內容已經發生了變化:
    // error: code = 500 reason = InternalError.DBConnection message = Updated message: retry failed metadata = map[request_id:abc-def trace_id:xyz-789 user_id:12345]
    // 元數據也會被一并輸出。
    fmt.Println(errx)

    // 調用 doSomething 函數,生成一個錯誤,并打印它,這里返回一個更新過 Message 字段的預定義錯誤 errno.ErrUsernameInvalid。
    someerr := doSomething()
    // 打印錯誤。
    // error: code = 400 reason = InvalidArgument.UsernameInvalid message = Username is too short metadata = map[]
    fmt.Println(someerr)

    // 調用預定義錯誤 errno.ErrUsernameInvalid 的 Is 方法,判斷 someerr 是否屬于該類型錯誤。
    // Is 方法會比較 Code 和 Reason 字段(不會比較 Message 字段),如果兩者一致,則返回 true。
    // 因為 doSomething 返回的錯誤正是 errno.ErrUsernameInvalid 的實例,因此這里輸出 true。
    fmt.Println(errno.ErrUsernameInvalid.Is(someerr))

    // 調用另外一個預定義錯誤 errno.ErrPasswordInvalid 的 Is 方法,比較 someerr 是否屬于該錯誤。
    // 因為 Reason 和 Code 不匹配(someerr 是 username 錯誤,而不是 password 錯誤),因此返回 false。
    fmt.Println(errno.ErrPasswordInvalid.Is(someerr))
}

// 定義一個函數 doSomething,返回一個錯誤
func doSomething() error {
    // 這里返回了一個已經定義的錯誤類型 errno.ErrUsernameInvalid,但動態地設置了 Message 字段為 "Username is too short"。
    // 重點是:雖然錯誤的 Message 不同,但錯誤的 Code 和 Reason 是一致的,這方便使用 Is 方法進行類型判斷而不受具體內容影響。
    return errno.ErrUsernameInvalid.WithMessage("Username is too short")
}

上述代碼已有詳盡的代碼注釋,這里不再詳細介紹。

四、總結

本節課探討了錯誤返回的優秀實踐,比較了兩種常見的錯誤返回方式,并選擇了更符合企業級開發需求的第二種方式。

通過定義 ErrorX 結構體,miniblog 項目實現了包含 HTTP/gRPC 狀態碼、業務錯誤碼、錯誤信息及元數據的錯誤返回機制。此外,還為 ErrorX 提供了便捷的字段設置方法,方便開發者快速構造和返回錯誤。

責任編輯:趙寧寧 來源: 令飛編程
相關推薦

2009-09-29 10:35:42

Linux系統系統提速Linux

2025-11-10 10:58:07

2015-07-29 10:25:05

數據開發產品必修課

2010-11-25 10:55:34

2025-05-22 08:25:00

C++開發資源管理

2025-02-05 11:00:00

開發Java對象模型

2014-02-17 09:22:37

2022-03-11 10:53:32

UML建模語言

2022-09-19 10:04:44

人工智能AIIT領導者

2022-08-15 15:03:57

數字化轉型數字技術中小企業

2009-02-10 15:08:41

2020-10-23 10:02:40

GRASPRDD模式

2018-08-06 11:07:03

技術管理者識人

2012-01-06 14:10:42

數據質量管理大數據數據管理

2023-09-27 22:18:41

2025-07-31 06:00:00

Go后端開發

2020-01-13 16:26:57

AI人工智能機器

2023-05-15 09:51:23

算力開發

2018-04-28 10:05:17

2025-09-17 07:07:00

智能體GenAI人工智能
點贊
收藏

51CTO技術棧公眾號

日韩一级视频免费观看在线| 樱桃国产成人精品视频| 国产精品久久婷婷六月丁香| 亚洲精品一区二区三区在线播放| crdy在线观看欧美| 亚洲午夜国产一区99re久久| 久久国产精品-国产精品| 波多野结衣在线观看一区| 亚洲91中文字幕无线码三区| 亚洲精品福利在线观看| a在线观看免费视频| 草草影院在线| 国产精品久久久久影院色老大| 97超碰在线播放| 免费黄色网址在线| 欧美一区高清| 伊人久久精品视频| 亚洲中文字幕无码一区| 国产精品成人国产| 丰满岳妇乱一区二区三区| 天天爱天天做天天操| 欧洲成人av| 成人免费三级在线| 成人国产在线视频| 日韩一级片中文字幕| 欧美日韩一区自拍| 色婷婷av一区二区三区久久| 污污内射在线观看一区二区少妇 | 国产99对白在线播放| 久久一区中文字幕| 亚州成人av在线| 丁香花五月激情| 色乱码一区二区三区网站| 日韩高清a**址| 少妇伦子伦精品无吗| 永久免费观看精品视频| 91高清视频免费看| 337p粉嫩大胆噜噜噜鲁| 精品一性一色一乱农村| 成人欧美一区二区三区黑人麻豆| 欧洲av一区| 免费在线观看一级毛片| 99久久777色| 精品高清视频| 天堂av资源网| 99久久精品一区| 国产99视频精品免费视频36| a天堂在线观看视频| 经典一区二区三区| 川上优av一区二区线观看| 一级黄色大片免费观看| 蜜臀av一区二区在线免费观看| 日韩**中文字幕毛片| 欧美精品一二三四区| 欧美亚洲三级| 日本精品久久久| 久久免费激情视频| 国产精品毛片一区二区三区| 国产91精品不卡视频| 欧美日韩综合在线观看| 国产精品久久久免费| 欧美一区二区三区四区在线| 久久久免费高清视频| 欧美亚洲三区| 国产精品入口免费视频一| 中文字幕+乱码+中文字幕明步 | 2025国产精品视频| 国产精品男女视频| 日韩电影在线看| 国产精品高潮呻吟视频| 亚洲自拍偷拍另类| 国精品**一区二区三区在线蜜桃 | youjizzxxxx18| 国精品产品一区| 7799精品视频| 亚洲成a人无码| 国产精品三级| 久久久精品一区| 国产精品成人aaaa在线| 久久国产精品久久w女人spa| 国产精品日日做人人爱| 午夜老司机福利| 91免费观看在线| 亚洲欧洲国产日韩精品| 欧美另类tv| 色噜噜狠狠色综合中国 | 国产精品资源在线看| 国产亚洲欧美一区二区 | a级日韩大片| 亚洲欧美另类国产| 日韩一卡二卡在线观看| 亚洲精品麻豆| 国产欧美精品xxxx另类| 亚洲精品久久久久久无码色欲四季 | av成人在线电影| 黑人乱码一区二区三区av| 国产香蕉久久精品综合网| 在线视频福利一区| 毛片在线网站| 91精品国产欧美一区二区18 | 日韩在线亚洲| 亚洲欧美三级在线| 成年人av电影| 日韩国产精品大片| 国产精品日韩二区| 香蕉视频网站在线观看| 亚洲午夜在线视频| 精品亚洲一区二区三区四区| 久久国产精品免费精品3p| 日韩最新免费不卡| 四虎成人在线观看| 国产成人免费视频网站| 亚洲一卡二卡三卡| 91av亚洲| 欧美精品一区二区三区很污很色的| 国产黄色录像视频| 欧美专区18| 国产精品一级久久久| 好吊日视频在线观看| 在线看一区二区| 人妖粗暴刺激videos呻吟| 婷婷伊人综合| 国产精品亚洲视频在线观看| 色久视频在线播放| 亚洲第一福利一区| 女人高潮一级片| 欧美老女人另类| 国产成人精品视频在线| 天天操天天射天天| 午夜影院在线观看欧美| 91人妻一区二区| 欧美在线首页| 91国产在线播放| 黄色网在线免费观看| 欧美探花视频资源| 69精品无码成人久久久久久| 亚洲在线网站| 麻豆精品视频| 中文在线免费视频| 精品一区二区三区四区| www.国产com| 91啪九色porn原创视频在线观看| 人妻夜夜添夜夜无码av| xxxx日韩| 国精产品一区一区三区有限在线| 亚洲av无码国产精品永久一区 | 亚洲精品视频久久| 亚洲天堂一区在线观看| 97久久超碰精品国产| 精品国产免费av| 妖精视频一区二区三区| 欧美亚洲第一页| 国产人成在线视频| 欧美日韩精品二区第二页| 四虎地址8848| 国产伦精品一区二区三区视频青涩 | 国产suv精品一区| 久久久久久久91| 天天射天天操天天干| 欧美日韩精品国产| 欧洲美熟女乱又伦| 精品在线免费视频| 9色视频在线观看| 国产精品久久久久久久久久白浆| 久久777国产线看观看精品| 午夜精品久久久久久久99老熟妇 | 日本福利视频一区| 欧美变态挠脚心| 国产精品91一区| 国产在线高潮| 日韩午夜中文字幕| 日韩av一二三区| 国产亚洲精品超碰| 三级性生活视频| 91成人观看| 国产精品久久久久免费| 午夜欧美激情| 久久精品最新地址| 日日夜夜精品免费| 欧洲精品视频在线观看| 91日韩中文字幕| 99国产精品久久久久| 视色视频在线观看| 国产精品sm| 欧美日韩精品久久久免费观看| 欧美特黄色片| 性色av一区二区三区免费| 福利在线播放| 欧美va亚洲va| 最近中文字幕在线视频| 一区二区三区毛片| 亚洲熟妇无码av| 国产一区二区福利视频| 免费看的黄色大片| 91av精品| 色婷婷精品国产一区二区三区| 免费观看亚洲天堂| 国产成人精品视频在线| 久草在线视频福利| 日韩在线视频线视频免费网站| 亚洲国产精品欧美久久| 欧美三级电影精品| 全部毛片永久免费看| 亚洲特级片在线| 日本精品在线观看视频| 成人夜色视频网站在线观看| 日本免费色视频| 久久精品午夜| 免费拍拍拍网站| 欧美韩日一区| 日本不卡二区高清三区| 国产极品模特精品一二| 91在线直播亚洲| www.久久.com| 日本一区二区三区在线播放| 电影k8一区二区三区久久| 精品国偷自产在线视频| 国产系列在线观看| 亚洲精品美女网站| 亚洲国产精品二区| 7777精品伊人久久久大香线蕉超级流畅 | 天天天天天天天干| 亚洲午夜久久久久久久久电影网| 伊人网在线视频观看| 成人精品免费看| 久久久九九九热| 免费在线看一区| 久草在在线视频| 亚洲资源av| 日本中文字幕网址| 一区三区视频| 97在线国产视频| 在线不卡欧美| 极品美女扒开粉嫩小泬| 一区二区三区国产盗摄| 青草青青在线视频| 尹人成人综合网| 日本午夜激情视频| 亚洲毛片视频| 欧美 丝袜 自拍 制服 另类| 国产字幕视频一区二区| 看全色黄大色大片| 你懂的国产精品永久在线| 欧美做受777cos| 欧美大片专区| 分分操这里只有精品| 亚洲激精日韩激精欧美精品| 精品少妇在线视频| 亚洲毛片播放| 日韩中文字幕二区| 日韩av在线免费观看不卡| 男人插女人下面免费视频| 奇米色777欧美一区二区| 中文字幕第80页| 精品亚洲成a人| 国内精品国产三级国产aⅴ久| 国产91在线|亚洲| 强迫凌虐淫辱の牝奴在线观看| 成人黄色大片在线观看| 日本黄色片在线播放| 久久久久久免费| 欧美人妻一区二区三区| 中文字幕一区二区三区在线播放| 99成人在线观看| 亚洲精品日产精品乱码不卡| 久久久精品视频在线| 五月婷婷激情综合网| 无码人妻一区二区三区免费| 欧美日韩色一区| 精品国产亚洲av麻豆| 精品国精品国产尤物美女| 午夜黄色小视频| 中文国产亚洲喷潮| 18在线观看的| 57pao成人国产永久免费| 日本一区二区三区视频在线| 成人观看高清在线观看免费| 成人台湾亚洲精品一区二区| 免费看成人片| 999久久久免费精品国产| 国产精品va在线观看无码| 国产精品一二| 国产精品嫩草影院8vv8| 懂色av一区二区在线播放| 人妻体内射精一区二区| 日韩理论在线观看| www.日本精品| 欧美一区二区三区播放老司机 | 色婷婷**av毛片一区| 国产深夜视频在线观看| 国产精品久久国产精品99gif| 久久精品九色| 欧美一级爱爱| 欧美破处大片在线视频| av视屏在线播放| 成人一级片网址| а天堂中文在线资源| 亚洲v欧美v另类v综合v日韩v| 一区二区乱子伦在线播放| 欧美女孩性生活视频| 免费a视频在线观看| 中文字幕亚洲情99在线| 欧美gv在线观看| 亚洲一区二区久久久久久| 奇米亚洲欧美| 日韩网站在线免费观看| 精品一区二区三区视频| 亚洲国产av一区| 亚洲国产精品影院| 91成品人影院| 伊人av综合网| 亚洲性色av| 国产高清在线精品一区二区三区| 日本久久一二三四| 精品国产免费av| 9色porny自拍视频一区二区| 国产大学生自拍| 在线播放视频一区| 在线免费观看黄色| 日本一区二区在线免费播放| 麻豆视频一区| 18禁网站免费无遮挡无码中文| 国产精品亚洲第一区在线暖暖韩国| 一级在线观看视频| 色综合久久久久综合体 | 日韩精品亚洲视频| 青春草免费在线视频| 91免费版网站入口| 国产精品久久久久久| 中文字幕国产传媒| 久久伊人中文字幕| 久久国产视频播放| 日韩av网址在线观看| 欧洲中文在线| 国产富婆一区二区三区| 亚洲经典视频在线观看| 精品人妻在线视频| 性感美女久久精品| 涩涩视频免费看| **欧美日韩vr在线| 欧美成人基地| 韩国日本在线视频| 久久久国产综合精品女国产盗摄| 久久精品视频7| 亚洲欧美日韩中文视频| 久久xxx视频| 亚洲一卡二卡区| 国产一区二区三区免费看| 2018天天弄| 欧美大胆一级视频| av老司机免费在线| 免费久久一级欧美特大黄| 久久久蜜桃一区二区人| 男人舔女人下部高潮全视频| 欧美性欧美巨大黑白大战| yw在线观看| 92国产精品久久久久首页 | www.浪潮av.com| 久久美女高清视频| 最近中文字幕在线观看| 久久久精品一区二区| 2020最新国产精品| 免费黄色福利视频| 国产精品青草久久| 国产哺乳奶水91在线播放| 97人人做人人爱| 精品视频网站| 国产精品熟女一区二区不卡| 午夜日韩在线观看| 99riav在线| 97在线电影| 亚洲永久免费精品| 手机在线中文字幕| 欧美成人官网二区| 欧美成人精品一区二区男人小说| 亚洲一二区在线| 成人免费黄色在线| 99精品人妻国产毛片| 久久久999国产| 日韩极品少妇| 999在线观看| 亚洲高清视频的网址| 岛国视频免费在线观看| 91精品国产一区二区三区动漫 | 91精产国品一二三产区别沈先生| 一区二区三区高清在线| 欧美黄色小说| 亚洲综合中文字幕在线观看| 久久福利毛片| 九九视频免费看| 中文字幕免费精品一区高清| 成人线上播放| av中文字幕网址| 欧美视频在线观看 亚洲欧| 黄色网在线看| 日本一区二区三不卡| 成人精品免费网站| 国产精品国产精品国产专区| 欧美性视频网站| 欧美网站在线|