This article is the 23rd day article of Go 3 Advent Calendar 2020.
I have been programming for about a year and a half, and usually work as an intern for a web programmer (Go, React).
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.
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!
OS | macOS Catalina 10.15.7 |
Go | go1.15.4 darwin/amd64 |
Homebrew | 2.6.1 |
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!)
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.
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]
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
)
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.
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.
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
GET
HEAD
POST
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH
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.
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.
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
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
.
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.
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
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
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!
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.
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!
Recommended Posts