Better Programming

Advice for programmers.

Developing a RESTful API With Go

Kevin Wan
7 min readMay 15, 2022

--

Photo by Douglas Lopes on Unsplash

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 increment, 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

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"

Account module API definition

  • View user details
  • Get all orders for a user
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)
}

Order module API definition

  • Get order details
  • Generate orders
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)
}

Product module API definition

  • View product details
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)
}

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 .

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

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...

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),
}
}

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
}

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
}

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

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"
}
]
}

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),
}
}

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
}

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
}

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.

Want to Connect?Welcome to use go-zero and star to support us!

--

--

Responses (2)