Goa

Design Overview

// goa.design/goa — API-first design framework
// Write DSL → Generate code (server, client, docs)
//
// Project layout:
//   design/   — API DSL definitions
//   gen/      — generated code (do not edit)
//   cmd/      — service entry points

package design

import . "goa.design/goa/v3/dsl"

API DSL

var _ = API("myapp", func() {
    Title("My Application")
    Description("A sample API")
    Version("1.0.0")

    Server("myapp", func() {
        Host("localhost", func() {
            URI("http://localhost:8080")
            URI("grpc://localhost:9090")
        })
    })
})

Service Definition

var _ = Service("users", func() {
    Description("User management service")

    Error("not_found", NotFound, "User not found")
    Error("unauthorized", String, "Unauthorized")

    HTTP(func() {
        Path("/users")
    })

    GRPC(func() {
        // gRPC transport mapping
    })

    Method("list", func() {
        Description("List all users")
        Result(CollectionOf(UserResult))
        GRPC(func() {
            Response(CodeOK)
        })
        HTTP(func() {
            GET("/")
            Response(StatusOK)
        })
    })
})

Method Definition

Method("create", func() {
    Description("Create a new user")
    Payload(CreateUserPayload)
    Result(UserResult)

    HTTP(func() {
        POST("/")
        Response(StatusCreated)
    })

    GRPC(func() {
        Response(CodeOK)
    })
})

Method("show", func() {
    Payload(func() {
        Field(1, "id", UInt, "User ID")
        Required("id")
    })
    Result(UserResult)

    HTTP(func() {
        GET("/{id}")
        Response(StatusOK)
    })
})

Payload & Result

var CreateUserPayload = Type("CreateUserPayload", func() {
    Field(1, "name", String, "User name", func() {
        MinLength(2)
        MaxLength(100)
    })
    Field(2, "email", String, "Email address", func() {
        Format(FormatEmail)
    })
    Field(3, "age", Int, "Age", func() {
        Minimum(0)
        Maximum(150)
    })
    Required("name", "email")
})

var UserResult = ResultType("application/vnd.user", func() {
    Field(1, "id", UInt, "User ID")
    Field(2, "name", String, "User name")
    Field(3, "email", String, "Email")
    Required("id", "name")
})

Error Definitions

var NotFound = Type("NotFound", func() {
    Field(1, "message", String, "Error message")
    Field(2, "id", UInt, "Resource ID")
    Required("message")
})

var _ = Service("users", func() {
    Error("not_found", NotFound, "User not found")

    Error("bad_request", func() {
        Field(1, "detail", String)
        Required("detail")
    })

    HTTP(func() {
        Response("not_found", StatusNotFound)
        Response("bad_request", StatusBadRequest)
    })

    GRPC(func() {
        Response("not_found", CodeNotFound)
        Response("bad_request", CodeInvalidArgument)
    })
})

HTTP Transport

Method("update", func() {
    Payload(func() {
        Field(1, "id", UInt)
        Field(2, "name", String)
    })
    Result(UserResult)

    HTTP(func() {
        PUT("/{id}")
        Header("Authorization:String")
        Response(StatusOK)
        Response("not_found", StatusNotFound)
    })
})

Method("upload", func() {
    Payload(func() {
        Field(1, "file", Bytes)
    })
    HTTP(func() {
        POST("/upload")
        MultipartRequest()
    })
})

Method("search", func() {
    Payload(func() {
        Field(1, "q", String)
        Field(2, "page", Int, func() { Default(1) })
    })
    HTTP(func() {
        GET("/search")
        Param("q")
        Param("page")
    })
})

gRPC Transport

var _ = Service("users", func() {
    GRPC(func() {
        Path("/proto/users")
    })

    Method("create", func() {
        Payload(CreateUserPayload)
        Result(UserResult)

        GRPC(func() {
            Message(func() {
                Field(1, "name", String)
                Field(2, "email", String)
            })
            Response(CodeOK)
            Response("not_found", CodeNotFound)
        })
    })

    Method("stream", func() {
        Payload(func() {
            Field(1, "filter", String)
        })
        Result(StreamingResult(UserResult))

        GRPC(func() {
            Response(CodeOK)
        })
    })
})

Goa gen Command

# Generate all (HTTP + gRPC + docs)
goa gen myapp/design

# Generate only HTTP
goa gen --http myapp/design

# Generate only gRPC
goa gen --grpc myapp/design

# Generate example client/server
goa example myapp/design

# Typical go generate directive
# go:generate goa gen myapp/design

Server Setup

package main

import (
    "myapp/gen/users"
    "goa.design/goa/v3/http"
)

func main() {
    svc := &UserService{}
    endpoints := users.NewEndpoints(svc)

    mux := http.NewMuxer()
    usersServer := users.NewHTTPServer(endpoints, mux)
    usersServer.Mount(mux)

    httpsvc := http.NewServer(mux)
    httpsvc.Addr = ":8080"

    if err := httpsvc.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

Client Generation

// Goa generates client code automatically

import "myapp/gen/client/users"

func main() {
    httpCl := http.NewClient("http://localhost:8080")
    cl := users.NewClient(httpCl)

    result, err := cl.List(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    user, err := cl.Show(context.Background(), &users.ShowPayload{ID: 1})
}

Middleware

// In the design DSL
Method("admin", func() {
    Payload(func() {
        TokenField("token", String)
        Required("token")
    })

    HTTP(func() {
        GET("/admin")
        Header("token:Authorization")
    })
})

// Server-side middleware
mux := http.NewMuxer()
usersServer := users.NewHTTPServer(endpoints, mux)
usersServer.Mount(mux)

// Wrap with middleware
handler := http.RequestEncoder(
    middleware.Logging(mux),
)

Validation

var CreateUserPayload = Type("CreateUserPayload", func() {
    Field(1, "name", String, func() {
        MinLength(2)
        MaxLength(100)
        Pattern("^[a-zA-Z ]+$")
    })
    Field(2, "email", String, func() {
        Format(FormatEmail)
    })
    Field(3, "age", Int, func() {
        Minimum(0)
        Maximum(150)
    })
    Field(4, "role", String, func() {
        Enum("admin", "user", "guest")
    })
    Field(5, "tags", ArrayOf(String), func() {
        MinLength(1)
        MaxLength(10)
    })
    Required("name", "email")
})

File Streaming

Method("download", func() {
    Result(func() {
        Field(1, "content", Bytes)
    })
    HTTP(func() {
        GET("/download/{id}")
        Response(StatusOK)
    })
})

Method("upload", func() {
    Payload(func() {
        Field(1, "file", Bytes)
        Field(2, "name", String)
    })
    HTTP(func() {
        POST("/upload")
        MultipartRequest()
        Response(StatusCreated)
    })
})

Method("events", func() {
    Result(StreamingResult(EventResult))
    HTTP(func() {
        GET("/events")
        Response(StatusOK)
    })
})

设计概述

// goa.design/goa — API 优先设计框架
// 编写 DSL → 生成代码(服务端、客户端、文档)
//
// 项目结构:
//   design/   — API DSL 定义
//   gen/      — 生成代码(勿手动修改)
//   cmd/      — 服务入口

package design

import . "goa.design/goa/v3/dsl"

API DSL

var _ = API("myapp", func() {
    Title("My Application")
    Description("A sample API")
    Version("1.0.0")

    Server("myapp", func() {
        Host("localhost", func() {
            URI("http://localhost:8080")
            URI("grpc://localhost:9090")
        })
    })
})

服务定义

var _ = Service("users", func() {
    Description("用户管理服务")

    Error("not_found", NotFound, "User not found")
    Error("unauthorized", String, "Unauthorized")

    HTTP(func() {
        Path("/users")
    })

    GRPC(func() {
        // gRPC 传输映射
    })

    Method("list", func() {
        Description("List all users")
        Result(CollectionOf(UserResult))
        GRPC(func() {
            Response(CodeOK)
        })
        HTTP(func() {
            GET("/")
            Response(StatusOK)
        })
    })
})

方法定义

Method("create", func() {
    Description("创建新用户")
    Payload(CreateUserPayload)
    Result(UserResult)

    HTTP(func() {
        POST("/")
        Response(StatusCreated)
    })

    GRPC(func() {
        Response(CodeOK)
    })
})

Method("show", func() {
    Payload(func() {
        Field(1, "id", UInt, "User ID")
        Required("id")
    })
    Result(UserResult)

    HTTP(func() {
        GET("/{id}")
        Response(StatusOK)
    })
})

Payload 与 Result

var CreateUserPayload = Type("CreateUserPayload", func() {
    Field(1, "name", String, "用户名", func() {
        MinLength(2)
        MaxLength(100)
    })
    Field(2, "email", String, "邮箱地址", func() {
        Format(FormatEmail)
    })
    Field(3, "age", Int, "年龄", func() {
        Minimum(0)
        Maximum(150)
    })
    Required("name", "email")
})

var UserResult = ResultType("application/vnd.user", func() {
    Field(1, "id", UInt, "用户 ID")
    Field(2, "name", String, "用户名")
    Field(3, "email", String, "邮箱")
    Required("id", "name")
})

错误定义

var NotFound = Type("NotFound", func() {
    Field(1, "message", String, "错误信息")
    Field(2, "id", UInt, "资源 ID")
    Required("message")
})

var _ = Service("users", func() {
    Error("not_found", NotFound, "User not found")

    Error("bad_request", func() {
        Field(1, "detail", String)
        Required("detail")
    })

    HTTP(func() {
        Response("not_found", StatusNotFound)
        Response("bad_request", StatusBadRequest)
    })

    GRPC(func() {
        Response("not_found", CodeNotFound)
        Response("bad_request", CodeInvalidArgument)
    })
})

HTTP 传输

Method("update", func() {
    Payload(func() {
        Field(1, "id", UInt)
        Field(2, "name", String)
    })
    Result(UserResult)

    HTTP(func() {
        PUT("/{id}")
        Header("Authorization:String")
        Response(StatusOK)
        Response("not_found", StatusNotFound)
    })
})

Method("upload", func() {
    Payload(func() {
        Field(1, "file", Bytes)
    })
    HTTP(func() {
        POST("/upload")
        MultipartRequest()
    })
})

Method("search", func() {
    Payload(func() {
        Field(1, "q", String)
        Field(2, "page", Int, func() { Default(1) })
    })
    HTTP(func() {
        GET("/search")
        Param("q")
        Param("page")
    })
})

gRPC 传输

var _ = Service("users", func() {
    GRPC(func() {
        Path("/proto/users")
    })

    Method("create", func() {
        Payload(CreateUserPayload)
        Result(UserResult)

        GRPC(func() {
            Message(func() {
                Field(1, "name", String)
                Field(2, "email", String)
            })
            Response(CodeOK)
            Response("not_found", CodeNotFound)
        })
    })

    Method("stream", func() {
        Payload(func() {
            Field(1, "filter", String)
        })
        Result(StreamingResult(UserResult))

        GRPC(func() {
            Response(CodeOK)
        })
    })
})

Goa 生成命令

# 生成全部(HTTP + gRPC + 文档)
goa gen myapp/design

# 仅生成 HTTP
goa gen --http myapp/design

# 仅生成 gRPC
goa gen --grpc myapp/design

# 生成示例客户端/服务端
goa example myapp/design

# 常用 go generate 指令
# go:generate goa gen myapp/design

服务端搭建

package main

import (
    "myapp/gen/users"
    "goa.design/goa/v3/http"
)

func main() {
    svc := &UserService{}
    endpoints := users.NewEndpoints(svc)

    mux := http.NewMuxer()
    usersServer := users.NewHTTPServer(endpoints, mux)
    usersServer.Mount(mux)

    httpsvc := http.NewServer(mux)
    httpsvc.Addr = ":8080"

    if err := httpsvc.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

客户端生成

// Goa 自动生成客户端代码

import "myapp/gen/client/users"

func main() {
    httpCl := http.NewClient("http://localhost:8080")
    cl := users.NewClient(httpCl)

    result, err := cl.List(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    user, err := cl.Show(context.Background(), &users.ShowPayload{ID: 1})
}

中间件

// 在设计 DSL 中
Method("admin", func() {
    Payload(func() {
        TokenField("token", String)
        Required("token")
    })

    HTTP(func() {
        GET("/admin")
        Header("token:Authorization")
    })
})

// 服务端中间件
mux := http.NewMuxer()
usersServer := users.NewHTTPServer(endpoints, mux)
usersServer.Mount(mux)

// 使用中间件包装
handler := http.RequestEncoder(
    middleware.Logging(mux),
)

数据验证

var CreateUserPayload = Type("CreateUserPayload", func() {
    Field(1, "name", String, func() {
        MinLength(2)
        MaxLength(100)
        Pattern("^[a-zA-Z ]+$")
    })
    Field(2, "email", String, func() {
        Format(FormatEmail)
    })
    Field(3, "age", Int, func() {
        Minimum(0)
        Maximum(150)
    })
    Field(4, "role", String, func() {
        Enum("admin", "user", "guest")
    })
    Field(5, "tags", ArrayOf(String), func() {
        MinLength(1)
        MaxLength(10)
    })
    Required("name", "email")
})

文件流

Method("download", func() {
    Result(func() {
        Field(1, "content", Bytes)
    })
    HTTP(func() {
        GET("/download/{id}")
        Response(StatusOK)
    })
})

Method("upload", func() {
    Payload(func() {
        Field(1, "file", Bytes)
        Field(2, "name", String)
    })
    HTTP(func() {
        POST("/upload")
        MultipartRequest()
        Response(StatusCreated)
    })
})

Method("events", func() {
    Result(StreamingResult(EventResult))
    HTTP(func() {
        GET("/events")
        Response(StatusOK)
    })
})