The Ops Community

Kevin Wan
Kevin Wan

Posted on

Developing a RESTful API with Go

When to use RESTful API

For most startups, we should focus more on delivering the products in the early stage of business. The monolithic services have the advantages of simple architecture, easy deployment, and better development productivity, which can help us achieve the product requirements quickly. While we use monolithic services to deliver products quickly, we also need to reserve the possibility for business incresement, so we usually split different business modules clearly in monolithic services.

Shopping mall monolithic service architecture

We take the mall as an example to build a monolithic service. The mall service is generally relatively complex and consists of multiple modules, the more important modules include account, product and order modules, etc. Each module will have its own independent business logic, and each module will also depend on some others. For example, the order module and the product module will depend on the account module. In the monolithic application this kind of dependency is usually accomplished by method calls between modules. Monolithic services generally share storage resources, such as MySQL and Redis.

The overall architecture of monolithic services is relatively simple, which is also the advantage of monolithic services. Customer requests are parsed through DNS and forwarded to the mall's backend services through Nginx. Mall services are deployed on cloud hosts. In order to achieve greater throughput and high availability, the service will generally deployed with multiple copies. This simple architecture can carry high throughput if well optimized.

For example, a request for order details interface /order/detail is routed to the order module, which relies on the account module and the product module to compose the complete order details back to the user, and multiple modules in a single service generally share the database and cache.

Monolithic Service

The next section describes how to quickly implement a mall monolithic service based on go-zero. Devs who have used go-zero know that we provide an API format file to describe the Restful API, and then we can generate the corresponding code by goctl with one command, we just need to fill in the corresponding business logic in the logic files. The mall service contains several modules, and in order to make the modules independent from each other, different modules are defined by separate APIs, but all the APIs are defined for the same service (mall-api).

Create user.api, order.api, product.api and mall.api in the api directory, where mall.api is the aggregated api file. Other api files are imported via import directives.

api
|-- mall.api
|-- order.api
|-- product.api
|-- user.api
Enter fullscreen mode Exit fullscreen mode

Mall API Definition

mall.api is defined as follows, where syntax = "v1" means that this is the v1 syntax of zero-api.

syntax = "v1"

import "user.api"
import "order.api"
import "product.api"
Enter fullscreen mode Exit fullscreen mode

Account module API definition

  • View user details
  • Get all orders for a user

user.api is defined as follows.

syntax = "v1"

type (
    UserRequest {
        ID int64 `path:"id"`
    }

    UserReply {
        ID      int64   `json:"id"`
        Name    string  `json:"name"`
        Balance float64 `json:"balance"`
    }

    UserOrdersRequest {
        ID int64 `path:"id"`
    }

    UserOrdersReply {
        ID       string `json:"id"`
        State    uint32 `json:"state"`
        CreateAt string `json:"create_at"`
    }
)

service mall-api {
    @handler UserHandler
    get /user/:id (UserRequest) returns (UserReply)

    @handler UserOrdersHandler
    get /user/:id/orders (UserOrdersRequest) returns (UserOrdersReply)
}
Enter fullscreen mode Exit fullscreen mode

Order module API definition

  • Get order details
  • Generate orders

order.api is defined as follows.

syntax = "v1"

type (
    OrderRequest {
        ID string `path:"id"`
    }

    OrderReply {
        ID       string `json:"id"`
        State    uint32 `json:"state"`
        CreateAt string `json:"create_at"`
    }

    OrderCreateRequest {
        ProductID int64 `json:"product_id"`
    }

    OrderCreateReply {
        Code int `json:"code"`
    }
)

service mall-api {
    @handler OrderHandler
    get /order/:id (OrderRequest) returns (OrderReply)

    @handler OrderCreateHandler
    post /order/create (OrderCreateRequest) returns (OrderCreateReply)
}
Enter fullscreen mode Exit fullscreen mode

Product module API definition

  • View product details

product.api is defined as follows.

syntax = "v1"

type ProductRequest {
    ID int64 `path:"id"`
}

type ProductReply {
    ID    int64   `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
    Count int64   `json:"count"`
}

service mall-api {
    @handler ProductHandler
    get /product/:id (ProductRequest) returns (ProductReply)
}
Enter fullscreen mode Exit fullscreen mode

Generating the monolithic service

With the API already defined, generating a service with the API becomes very simple, we use goctl to generate the monolithic service code.

$ goctl api go -api api/mall.api -dir .
Enter fullscreen mode Exit fullscreen mode

The generated code is structured as follows.

.
├── api
 ├── mall.api
 ├── order.api
 ├── product.api
 └── user.api
├── etc
 └── mall-api.yaml
├─ internal
 ├── config
  └── config.go
 ├── handler
  ├── ordercreatehandler.go
  ├── orderhandler.go
  ├── producthandler.go
  ├── routes.go
  ├── userhandler.go
  └─ userordershandler.go
 ├─ logic
  ├─ ordercreatelogic.go
  ├── orderlogic.go
  ├── productlogic.go
  ├── userlogic.go
  └── userorderslogic.go
 ├── svc
  └── servicecontext.go
 └── types
 └── types.go
└── mall.go
Enter fullscreen mode Exit fullscreen mode

Let's explain the generated files.

  • api: holds the API description file
  • etc: used to define the project configuration, all configuration items can be written in mall-api.yaml
  • internal/config: the configuration definition of the service
  • internal/handler: the implementation of the handler corresponding to the routes defined in the API file
  • internal/logic: used to put the business logic corresponding to each route, the reason for the distinction between handler and logic is to make the business processing part as less dependent as possible, to separate HTTP requests from the logic processing code, and to facilitate the subsequent splitting into RPC service
  • internal/svc: used to define the dependencies of the business logic processing, we can create the dependent resources in the main function and pass them to handler and logic via ServiceContext
  • internal/types: defines the API request and response data structures
  • mall.go: the file where the main function is located, with the same name as the service in the API definition, minus the -api suffix

The generated service can be run without any modification:

$ go run mall.go
Starting server at 0.0.0.0:8888...
Enter fullscreen mode Exit fullscreen mode

Implementing the business logic

Next, let's implement the business logic. The logic will be simple for demonstration purposes, not real business logic.

First, let's implement the logic of getting all orders for users. Since there is no order-related information in the user module, we need to rely on the order module to query the orders of users, so we add a dependency on OrderLogic in UserOrdersLogic.

type UserOrdersLogic struct {
    logx.Logger
    ctx        context.Context
    svcCtx     *svc.ServiceContext
    orderLogic *OrderLogic
}

func NewUserOrdersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserOrdersLogic {
    return &UserOrdersLogic{
        Logger:     logx.WithContext(ctx),
        ctx:        ctx,
        svcCtx:     svcCtx,
        orderLogic: NewOrderLogic(ctx, svcCtx),
    }
}
Enter fullscreen mode Exit fullscreen mode

Implement a method in OrderLogic to query all orders based on user id

func (l *OrderLogic) ordersByUser(uid int64) ([]*types.OrderReply, error) {
    if uid == 123 {
        // It should actually be queried from database or cache
        return []*types.OrderReply{
            {
                ID:       "236802838635",
                State:    1,
                CreateAt: "2022-5-12 22:59:59",
            },
            {
                ID:       "236802838636",
                State:    1,
                CreateAt: "2022-5-10 20:59:59",
            },
        }, nil
    }

    return nil, nil
}
Enter fullscreen mode Exit fullscreen mode

Call the ordersByUser method in the UserOrders method of UserOrdersLogic.

func (l *UserOrdersLogic) UserOrders(req *types.UserOrdersRequest) (*types.UserOrdersReply, error) {
    orders, err := l.orderLogic.ordersByUser(req.ID)
    if err ! = nil {
        return nil, err
    }

    return &types.UserOrdersReply{
        Orders: orders,
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

At this point we restart the mall-api service and request all the user's orders in the browser.

http://localhost:8888/user/123/orders
Enter fullscreen mode Exit fullscreen mode

The return result is as follows, as we expected

{
    "orders": [
        {
            "id": "236802838635",
            "state": 1,
            "create_at": "2022-5-12 22:59:59"
        },
        {
            "id": "236802838636",
            "state": 1,
            "create_at": "2022-5-10 20:59:59"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Next we'll implement the logic for creating an order. To create an order we first need to see if the item in stock is enough, so we need to rely on the item module in the order module.

type OrderCreateLogic struct {
    logx.Logger
    ctx          context.Context
    svcCtx       *svc.ServiceContext
    productLogic *ProductLogic
    productLogic *ProductLogic
}

func NewOrderCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrderCreateLogic {
    return &OrderCreateLogic{
        Logger:       logx.WithContext(ctx),
        ctx:          ctx,
        svcCtx:       svcCtx,
        productLogic: NewProductLogic(ctx, svcCtx),
    }
}
Enter fullscreen mode Exit fullscreen mode

The logic for creating an order is as follows.

const (
    success = 0
    failure = -1
)

func (l *OrderCreateLogic) OrderCreate(req *types.OrderCreateRequest) (*types.OrderCreateReply, error) {
    product, err := l.productLogic.productByID(req.ProductID)
    if err ! = nil {
        return nil, err
    }

    if product.Count > 0 {
        return &types.OrderCreateReply{Code: success}, nil
    }

    return &types.OrderCreateReply{Code: failure}, nil
}
Enter fullscreen mode Exit fullscreen mode

The logic of the dependent product module is as follows.

func (l *ProductLogic) Product(req *types.ProductRequest) (*types.ProductReply, error) {
    return l.productByID(req.ID)
}

func (l *ProductLogic) productByID(id int64) (*types.ProductReply, error) {
    return &types.ProductReply{
        ID: id,
        Name: "apple watch 3",
        Price: 3333.33,
        Count: 99,
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

The above shows that using go-zero to develop a monolithic service is very simple, which helps us to develop quickly. And we also separated modules, which also provides the possibility of changing to microservices later.

Summary

The above example shows that it is very simple to use go-zero to develop monolithic services. You only need to define the api file, and then the goctl tool can automatically generate the project code. We only need to fill in the business logic code in the logic package. In this article we just demonstrated how to quickly develop monolithic services based on go-zero, which does not involve databases. In fact, goctl can also generate CRUD and cache code with one command.

And for different business scenarios, customization can also be achieved through customizing templates. And customized templates can be shared within the team through remote git repositories, which can be very efficient for team collaboration.

Project address

https://github.com/zeromicro/go-zero

Welcome to use go-zero and star to support us!

Discussion (0)