An introduction to Web API development for those who have completed the Progate Go course

Introduction

This article is the 23rd day article of Go 3 Advent Calendar 2020.

Who are you?

I have been programming for about a year and a half, and usually work as an intern for a web programmer (Go, React).

Why did you come up with an article like this

This article is a guide for those who have completed the Progate Go course but don't know how to learn from now on.

I myself was taken care of by Progate when I was a complete programming beginner, but I remember being confused because I didn't know how to learn after completing all the Go courses.

Although Progate's language introduction phrase says "Google's growing popularity of server-side languages," Progate's lessons are for learning basic grammar and there is no guide to creating web apps or web APIs. A Tour of Go, which is said to be "good to do!" In the streets, is difficult and difficult to reach, and this is also mainly for learning grammar and does not solve the problem. Hmm.

As a person who wants to teach Go to beginners, I thought it was not good that there was no guide for beginners to learn how to make Web API in Japanese, so I decided to write this article.

We are planning to make corrections steadily, so if you have any suggestions for chapters that you would like experts to add, please leave a comment or Twitter!

Note: Please note that there are many areas where rigor is lacking for clarity.

Audience of this article

As the title suggests, it is intended for people (beginners in programming) who have completed the Go course of Progate and have studied Go grammar for a while.

In particular

--Understand the contents up to Progate Go IV + Understand export, map, struct, method --Understand the basic commands in the CLI (it's okay if you've completed Progate's Command line course)

It is assumed that.

If you came immediately after finishing Progate, or if you are not doing Progate, please read and understand the following article.

-[Go] Basic Grammar ① (Basic) -[Go] Basic Grammar ② (Flow Control Statement) -[Go] Basic Grammar ③ (Pointer / Structure) -[Go] Basic Grammar ④ (Array / Slice) -[Go] Basic Grammar ⑤ (Associative Array / Range)

It's been a long time since I started the Go course of Progate, so I haven't fully grasped what I learned in Progate. If you meet the above conditions and cannot understand something, please let us know in the comments and Twitter as well as the suggestions in the chapter!

Writer's environment

OS macOS Catalina 10.15.7
Go go1.15.4 darwin/amd64
Homebrew 2.6.1

table of contents

  1. Go environment construction
  2. Hello Web API
  3. Development of ToDo API

Go environment construction

Since I only have a Mac at hand, I will only post how to build an environment on a Mac. (Because there is a risk of misrepresenting wrong or old information) (If you are an expert, please share your request for editing the environment construction on Linux/Windows and recommended articles!)

Download and install Go

There are various types, but in this article, we will use Homebrew with an emphasis on ease of use.

Please refer to Official Site for the installation of Homebrew itself.

It's done with just one line of command.

brew install go

When you're done, try running the command go version.

go version go1.15.4 darwin/amd64

Is displayed, the process is complete.

GOPATH / environment variable settings

I won't explain why it is necessary, but if you are interested, you should read the article Go development environment that does not depend on GOPATH (as much as possible) (Go 1.15 version).

Create the required directories. All you have to do is execute the following command.

mkdir -p $HOME/go/src $HOME/go/pkg $HOME/go/bin

Make sure to set the environment variables required for Go when starting the shell. Execute the command echo $ SHELL and copy the following text to $ HOME/.bash_profile if it ends with bash, or to $ HOME/.zshrc if it ends with zsh. ..

If the file does not exist, create it.

export GOPATH="$HOME/go"
export GOBIN="$GOPATH/bin"
export GO111MODULE=auto

Hello World in your local environment

Now that the environment has been built, let's check the operation immediately!

First, create an appropriate directory.

mkdir $HOME/Desktop/first-go

Initialize the Go Module after navigating to that directory. As for Go Module, it's okay if you think it's a good practice now and move on.

It is used for Go project packages and their version control.

go mod init [module]

It's customary to set the [module] to the URL of the repository for that project, so if you have a GitHub account, it's a good idea to use go mod init github.com/[username]/first-go.

If you don't have it, you can use go mod init first-go!

Please refer to the [module] that appears in the future for your environment.

If a file called go.mod is created, it is successful.

Then create a file called main.go and copy and paste the code below.

main.go


package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}

If you can copy and paste, try running the code with the command go run main.go. go run is a command that compiles and executes when you specify a file with the main package main function.

Hello World!

Is displayed, the environment construction is complete!

Hello Web API

First, create a directory and initialize the Go Module.

mkdir $HOME/Desktop/first-web-api
cd $HOME/Desktop/first-web-api
go mod init [module]

Download external package

After initialization, download the external package to be used this time. It can be implemented with a standard package, but in this article, I will use a framework called Gin to learn how to use an external package. In addition, we will use a task ID called UUID, so download the package to generate it.

Execute the following command.

go get github.com/gin-gonic/gin
go get github.com/google/uui

Downloading external packages in Go is done with the go get command.

If a file called go.sum is generated and go.mod is rewritten as below, it is successful.

go.mod


module [module]

go 1.15

require (
	github.com/gin-gonic/gin v1.6.3 // indirect
	github.com/google/uuid v1.1.2 // indirect
)

For the time being, try Hello World with Web API without thinking about anything

I will write in the flow of copying sutras → moving → commentary due to brain death. Create main.go and copy and paste the code below.

main.go


package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()

    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "Hello World!")
    })

    r.Run()
}

After running with go run main.go, try accessing localhost: 8080/hello with any browser.

If you see Hello World!, You are successful!

If you can confirm it, press Ctrl + C to stop the program once.

Commentary

the first

r := gin.Default()

Is initializing the Router. It's okay if you think that the Router registers where to access and what to do (handler).

next

r.GET("/hello", func(c *gin.Context) {
    c.String(200, "Hello World!")
})

Is registering a handler for Router. When a request comes to "/hello " with the GET method, the process of returning the character string"Hello World"with status code 200 is described.

The first argument is an argument like "/hello ", and the second argument is an argument like func (c * gin.Context) (c * gin.Context) Function.

Suddenly, several words that I don't understand come out, and I think that "!?" Is floating in the readers' minds, like Shonen Magazine, so I will explain them.

HTTP

These are HTTP terms, such as GET methods and status codes.

HTTP is a type of communication protocol, according to a small and difficult explanation of MDN Web Docs.

Hypertext Transfer Protocol (HTTP) is an application layer protocol for transferring hypermedia documents such as HTML. This protocol is designed for communication between web browsers (clients) and web servers, but it may also be used for other purposes. HTTP follows the traditional client-server model, where the client opens a port to send a request to the server and waits for a response from the server side. HTTP is a so-called stateless protocol, which means that the server holds no data between the two requests. HTTP is often used for communication over the TCP/IP layer, but it is also used for any reliable transport layer, ie protocols such as UDP where messages are not lost unknowingly. May be done. RUDP — UDP with reliability added — is also suitable as an alternative.

That being said, for the time being, it's okay to recognize that "the great people (IETF) have decided how to communicate with the browser and the Web server".

Most of the web servers that exist in this world are HTTP servers, and the web API created in this article is also an HTTP server. That's why the HTTP term came out.

Method

So, after all, what is the GET method? It's a kind of HTTP method. The HTTP method is meant to tell you what kind of request the request is when you send it to the HTTP server.

HTTP method

There are only 9 types in total.

GET is the most basic method and is the method used when you want to get a resource. For example, when accessing this Qiita article, the browser is sending a request to https://qiita.com/yuzuy/items/cf018bd8adaed1f55c84 with the GET method. The GET method is used when accessing by directly entering the address in the browser, and since the request was also sent to localhost: 8080/hello by the GET method, the character stringHello World! Was displayed.

Status code

The status code is the opposite of the method, because the HTTP server informs the HTTP client (browser) of the status.

The previous program returned 200, but 200 is OK, that is, the code that indicates the success of the request. There are quite a few status codes, but it's okay to roughly remember that 200 are normal, 400 are bad clients, and 500 are bad servers.

I think MDN Web Docs is the most accurate, but it is easy to understand even if you look at the sites explained with animal images such as HTTP Cats and HTTP Status Dogs. maybe.

404 Not Found is famous.

Now that I've briefly explained HTTP, I'll return to the code explanation.

Even so

r.Run()

It just starts the server.

ToDo API development

Now that we have finished Hello World with Web API, we will develop a slightly more advanced API.

In this chapter, we will develop an API for ToDo apps with the following functions.

--Get list of tasks --Add task --Task editing --Delete task

Add task

Even if you can not add a task and develop a task list acquisition API, nothing will come back, so first implement the task addition.

You can use the previous project, or if you want to divide it, you can create a new one.

First, decide on the structure of the task. Create a new directory called todo under the directory with main.go and create a file called task.go under it.

This time the task is

Let's have an element of.

Let's define Task in todo.go.

todo/task.go


import "time"

type Task struct {
    ID        string
    Name      string
    IsDone    bool
    CreatedAt time.Time
}

time.Time is a type that represents the time defined in the standard package time.

I've finished defining Task, but I don't have a place to save it. So let's define DB next. The initialization function is also defined.

Create a directory called db and create a file called db.go. This time, it is managed by an associative array with id as the key.

Specify [module]/todo to import the todo package.

db/db.go


package db

import "[module]/todo"

type DB struct {
    m map[string]*todo.Task
}

func New() *DB {
    return &DB{
        m: make(map[string]*todo.Task),
    }
}

Let's implement a method to add a task.

db/db.go


package db

import (
    "errors"

    "[module]/todo"
)

type DB struct {
    m map[string]*todo.Task
}

func New() *DB {
    return &DB{
        m: make(map[string]*todo.Task),
    }
}

func (db *DB) func AddTask(task *todo.Task) error {
    //nil check
    if task == nil {
        return errors.New("this task is nil")
    }

    //Prevent adding tasks with the same ID
    _, ok := db.m[task.ID]
    if ok {
        return errors.New("this task already added")
    }

    db.m[task.ID] = task

    return nil
}

Go returns error in the return value of the function to perform error handling. This time, when task is nil, it returns an error when ID tries to add the same task. According to Why you don't have to think about the possibility of UUID (v4) colliding, the expected value of UUID to be used in the task ID is 230 K times, but I suffered it on Twitter the other day. There were people (I forgot who Tweeted), so I'm going to follow Murphy's Law here.

When calling

err := db.AddTask(nil)
if err != nil {
    log.Println(err)
    return
}

Process as follows.

Basically all functions that can fail should return error.

This time, give * gin.Engine (Router) and * db.DB to the structure called Server, implement the handler with the method of Server, and make it Router with Server.Start () Register the handler and start it.

I think it's easier to understand this by looking at the code than by explaining it in words, so even if you don't understand it very well, it's a good idea to go a little further.

First, create a directory called server and a file called server.go under it. Define Server and implement the initialization function.

server/server.go


package server

import (
    "[module]/db"

    "github.com/gin-gonic/gin"
)

type Server struct {
    r  *gin.Engine
    db *db.DB
}

func New() *Server {
    return &Server{
        r:  gin.Default(),
        db: db.New(),
    }
}

As for the handler of Gin, (* gin.Context) should be the function or method of the argument, so define the method Server.AddTasks (c * gin.Context) and the handler for the endpoint to which the task is added. will do.

Requests for adding tasks should be received by POST. POST is a method that creates a resource. In POST, data can be entered in the place called request body, so ask the information of the new task to be entered in the format application/x-www-form-urlencoded, and add the task by referring to it. I will continue.

server/server.go


import (
    //Add to existing import
    "time"

    "github.com/google/uuid"
)

func (s *Server) addTask(c *gin.Context) {
    // application/x-www-form-c when receiving a request with urlencoded.You can retrieve the value with a method called PostForm
    name := c.PostForm("name")
    //If name is empty
    if name == "" {
        name = "untitled"
    }
    task := &todo.Task{
        ID:        uuid.New().String(), //uuid generation
        Name:      name,
        CreatedAt: time.Now(), //Get current time
    }

    err := s.db.AddTask(task)
    if err != nil {
        // err.Error()You can retrieve the error message with
        //In this article all errors return status code 500
        c.String(500, err.Error())
        return
    }

    //201 is Created, a status code that indicates that it was created
    c.String(201, "Created")
}

Now that we have implemented the handler, we will implement Server.Start () to register the handler and start the server.

server/server.go


func (s *Server) Start() error {
    s.r.POST("/tasks", s.addTask)

    //I didn't handle it earlier, but the Run method is actually returning an error
    return s.r.Run()
}

It's a little more. Just initialize and start Server with main.

main.go


pakcage main

import (
    "log"

    "[module]/server"
)

func main() {
    s := server.New()
    err := s.Start()
    if err != nil {
        log.Println(err)
    }
}

Now run go run main.go and it should start! I think it was a little long, but since I was able to lay the base for implementing other APIs, it became easier to add functions from the next time!

Sending a POST request in a browser is a bit tedious, so we'll test it using the command cURL.

If the following character string is displayed with the following command, it is successful.

command


curl -X POST -d 'name=hoge' -v http://localhost:8080/tasks

-X is the option to specify the method, -d is the option to enter the request body, and -v is the option to view the interaction in more detail.

result


*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /tasks HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 5
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 5 out of 5 bytes
< HTTP/1.1 201 Created
< Content-Type: text/plain; charset=utf-8
< Date: Wed, 23 Dec 2020 13:44:40 GMT
< Content-Length: 7
< 
* Connection #0 to host localhost left intact
Created* Closing connection 0

If you read it carefully, you'll see that POST is requesting/tasks and that the server is returning 201.

Challenge

I can't see the task list as it is, so I don't know if the program is really working properly. Let's output a log somewhere in the code to see if it's working properly.

Get task list

Currently, you can only see the task list in the log, so let's implement an API to get the task list so that you can see it in the front end (web application or mobile application)!

First, implement a new method to get to db.DB.

db/db.go


func (db *DB) FindTasks() []*todo.Task {
    //The number of elements that go into tasks is len(db.m)I know that, so if you secure the cap for that amount first, the processing will be a little faster.
    tasks := make([]*todo.Task, 0, len(db.m))

    for _, v := range db.m {
        tasks = append(tasks, v)
    }

    return tasks
}

All you have to do is implement the handler method in Server and add the registration in Server.Start!

server/server.go


func (s *Server) findTasks(c *gin.Context) {
    tasks := s.db.FindTasks()

    //Until the last time String(text)Returned the value in JSON format, which is easy to handle as a front end.
    //It is not good that the default format is different with the same API, so it is better to return addTask as JSON.
    c.JSON(200, tasks)
}

server/server.go


func (s *Server) Start() error {
    s.r.GET("/tasks", s.findTasks)
    s.r.POST("/tasks", s.addTask)

    return s.r.Run()
}

Now restart the server and you should be able to use the task list API!

Let's check using cURL! This is designed to receive requests with GET, so you can also check it in your browser.

When checking with cURL, GET is used by default, so the -X option is unnecessary.

command


curl -v http://localhost:8080/tasks

Challenge

In this case, the JSON field of the response becomes camel case like IsDone and CreatedAt. Example ↓

[
  {
    "ID": "43176919-b8d6-4811-a78c-67f94a79be4e",
    "Name": "untitiled",
    "IsDone": false,
    "CreatedAt": "2020-12-23T23:27:38.239276+09:00"
  }
]

Since it is customary to use all lowercase snake cases for the JSON field (most API responses we've seen so far have been snake cases), let's try to return a response like the one below.

[
  {
    "id": "43176919-b8d6-4811-a78c-67f94a79be4e",
    "name": "untitiled",
    "is_done": false,
    "created_at": "2020-12-23T23:27:38.239276+09:00"
  }
]

This is possible by adding the json tag to the field of the Task structure. Let's refer to the article below! Basics of tags that add meta information to Go structures

Delete task

If you leave it as it is, you can't do anything if you add the wrong task. At this rate, unnecessary tasks will continue to accumulate forever.

Let's implement the task deletion API so that you can delete tasks!

As with getting a list, first add a method to db.DB.

db/db.go


func (db *DB) RemoveTask(id string) error {
    //Returns an error when trying to delete a task that does not exist
    _, ok := db.m[id]
    if !ok {
        return errors.New("this task not found")
    }

    // db.Delete the element whose id is key from m
    delete(db.m, id)

    return nil
}

Next, add a method to Server and update Server.Start.

server/server.go


func (s *Server) Start() error {
    s.r.GET("/tasks", s.findTasks)
    s.r.POST("/tasks", s.addTask)
    //DELETE is the method used to delete a resource
    //The head of the endpoint":"By setting, the value becomes a parameter that can be determined by the client and can be referenced by the handler.
    s.r.DELETE("/tasks/:id", s.removeTask)

    return s.r.Run()
}

server/server.go


func (s *Server) removeTask(c *gin.Context) {
    //Get id parameter
    // /tasks/If a request comes to hoge, id"hoge"Enter
    id := c.Param("id")
    err := s.db.RemoveTask(id)
    if err != nil {
        // gin.H is map[string]interface{}Is the same as
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }

    c.JSON(200, gin.H{})
}

Let's restart the server again and delete any tasks!

Task editing

Let's implement this function by yourself by taking advantage of what we have learned so far!

Let's implement according to the following specifications.

--Receive parameters by URL like task deletion API --Receive a request with the PATCH method --Receive the request body with application/x-www-form-urlencoded --Allows the information of name and is_done to be updated --Returns an error if name is empty

An implementation example can be found in yuzuy/go-guide-after-progate, so please refer to it if you want to compare it with your own implementation or if you really don't understand.

Summary

In this article, I tried to build Go environment, Hello World with Web API, and implement simple Web API. We hope that our readers will grow as much as possible through this article.

In the future, I'm thinking of adding explanations for extending the ToDo API using RDBMS. I wrote it at the beginning, but if you have any concerns or suggestions, please leave a comment or DM!

Have a nice year!

Reference article / citation source

Recommended Posts

An introduction to Web API development for those who have completed the Progate Go course
Pepper Web API cooperation explanation For those who have no experience in Pepper application development / programming
[For those who want to use TPU] I tried using the Tensorflow Object Detection API 2
An introduction to the modern socket API to learn in C
Create an alias for Route53 to CloudFront with the AWS API
An introduction to Mercurial for non-engineers
An introduction to Python for non-engineers
An introduction to self-made Python web applications for a sluggish third-year web engineer
An introduction to OpenCV for machine learning
An introduction to Python for machine learning
An introduction to Python for C programmers
Join Azure Using Go ~ For those who want to start and know Azure with Go ~
An introduction to machine learning for bot developers
An introduction to object-oriented programming for beginners by beginners
Introduction to Python Let's prepare the development environment
An introduction to Cython that doesn't go deep
For those who have done pip uninstall setuptools
An introduction to statistical modeling for data analysis
Get an access token for the Pocket API
[Introduction to Python3 Day 20] Chapter 9 Unraveling the Web (9.1-9.4)
An introduction to voice analysis for music apps
Understand Python for Pepper development. -Introduction to Python Box-
An introduction to Cython that doesn't go deep -2-
[For those who have mastered other programming languages] 10 points to catch up on Python points
For those who are new to programming but have decided to analyze data with Python
For those who should have installed janome properly with Python but get an error