init.
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.env
|
||||
.envrc
|
||||
mysql/
|
||||
run_db.sh
|
||||
30
db.go
Normal file
30
db.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func openDB() (*sql.DB, error) {
|
||||
dsn := os.Getenv("MYSQL_DSN")
|
||||
if dsn == "" {
|
||||
log.Fatal("MYSQL_DSN is empty!")
|
||||
}
|
||||
return sql.Open("mysql", dsn)
|
||||
}
|
||||
|
||||
func migrate(ctx context.Context, db *sql.DB) error {
|
||||
ddl := `
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
age INT NOT NULL,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`
|
||||
_, err := db.ExecContext(ctx, ddl)
|
||||
return err
|
||||
}
|
||||
8
go.mod
Normal file
8
go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module work3
|
||||
|
||||
go 1.25.4
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
)
|
||||
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
197
handler.go
Normal file
197
handler.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
ok(w, &map[string]string{"status": "UP", "time": now})
|
||||
}
|
||||
|
||||
func studentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
listStudents(w, r)
|
||||
case http.MethodPost:
|
||||
createStudent(w, r)
|
||||
default:
|
||||
w.Header().Set("Allow", "GET, POST")
|
||||
fail(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func studentByIDHandler(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := strings.TrimPrefix(r.URL.Path, "/students/")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "invalid student id")
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
getStudent(w, r, id)
|
||||
case http.MethodPut:
|
||||
updateStudent(w, r, id)
|
||||
case http.MethodDelete:
|
||||
deleteStudent(w, r, id)
|
||||
default:
|
||||
w.Header().Set("Allow", "GET, PUT, DELETE")
|
||||
fail(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getStudent(w http.ResponseWriter, r *http.Request, id int64) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var s Student
|
||||
|
||||
err := db.QueryRowContext(ctx,
|
||||
`SELECT id,name,age,email,created_at,updated_at FROM students WHERE id=?`, id,
|
||||
).Scan(&s.ID, &s.Name, &s.Age, &s.Email, &s.CreatedAt, &s.UpdatedAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
fail(w, http.StatusNotFound, "student not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "db query error: "+err.Error())
|
||||
return
|
||||
}
|
||||
ok(w, &s)
|
||||
}
|
||||
|
||||
func updateStudent(w http.ResponseWriter, r *http.Request, id int64) {
|
||||
if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(strings.ToLower(ct), "application/json") {
|
||||
fail(w, http.StatusUnsupportedMediaType, "Content-Type must be application/json")
|
||||
return
|
||||
}
|
||||
|
||||
var in struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
fail(w, http.StatusBadRequest, "invalid json: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := db.ExecContext(ctx,
|
||||
`UPDATE students SET name=?, age=?, email=? WHERE id=?`,
|
||||
in.Name, in.Age, in.Email, id)
|
||||
if err != nil {
|
||||
if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
|
||||
fail(w, http.StatusConflict, "email already exists")
|
||||
return
|
||||
}
|
||||
fail(w, http.StatusInternalServerError, "update error: "+err.Error())
|
||||
return
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
fail(w, http.StatusNotFound, "student not found")
|
||||
return
|
||||
}
|
||||
|
||||
getStudent(w, r, id)
|
||||
}
|
||||
|
||||
func deleteStudent(w http.ResponseWriter, r *http.Request, id int64) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := db.ExecContext(ctx, `DELETE FROM students WHERE id=?`, id)
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "delete error: "+err.Error())
|
||||
return
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
fail(w, http.StatusNotFound, "student not found")
|
||||
return
|
||||
}
|
||||
|
||||
ok(w, &map[string]any{"deleted": id})
|
||||
}
|
||||
|
||||
func createStudent(w http.ResponseWriter, r *http.Request) {
|
||||
if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(strings.ToLower(ct), "application/json") {
|
||||
fail(w, http.StatusUnsupportedMediaType, "Context-Type must be application/json")
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
fail(w, http.StatusBadRequest, "invalid json: "+err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.Name) == "" || in.Age <= 0 || strings.TrimSpace(in.Email) == "" {
|
||||
fail(w, http.StatusBadRequest, "name/age/email required")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := db.ExecContext(ctx,
|
||||
`INSERT INTO students(name, age, email) VALUES(?,?,?)`,
|
||||
in.Name, in.Age, in.Email)
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "insert error: "+err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
|
||||
var out Student
|
||||
err = db.QueryRowContext(ctx,
|
||||
`SELECT id,name,age,email,created_at,updated_at FROM students WHERE id=?`, id,
|
||||
).Scan(&out.ID, &out.Name, &out.Age, &out.Email, &out.CreatedAt, &out.UpdatedAt)
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "fetch created row error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
created(w, "/students/"+strconv.FormatInt(out.ID, 10), &out)
|
||||
}
|
||||
|
||||
func listStudents(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
|
||||
defer cancel()
|
||||
rows, err := db.QueryContext(ctx,
|
||||
`SELECT id,name,age,email,created_at,updated_at FROM students ORDER BY id`)
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "db query error: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []Student
|
||||
for rows.Next() {
|
||||
var s Student
|
||||
if err := rows.Scan(&s.ID, &s.Name, &s.Age, &s.Email, &s.CreatedAt, &s.UpdatedAt); err != nil {
|
||||
fail(w, http.StatusInternalServerError, "scan error: "+err.Error())
|
||||
return
|
||||
}
|
||||
items = append(items, s)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
fail(w, http.StatusInternalServerError, "rows error: "+err.Error())
|
||||
return
|
||||
}
|
||||
ok(w, &items)
|
||||
}
|
||||
45
main.go
Normal file
45
main.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
type Student struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/health", healthHandler)
|
||||
mux.HandleFunc("/students", studentsHandler)
|
||||
mux.HandleFunc("/students/", studentByIDHandler)
|
||||
|
||||
var err error
|
||||
db, err = openDB()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err = migrate(context.Background(), db); err != nil {
|
||||
log.Fatal("migrate error: "+err.Error())
|
||||
}
|
||||
|
||||
addr := ":8080"
|
||||
log.Printf("Start server at %s\n", addr)
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
31
respose.go
Normal file
31
respose.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Result[T any] struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data *T `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func writeJSON[T any](w http.ResponseWriter, status int, payload Result[T]) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
|
||||
func ok[T any](w http.ResponseWriter, data *T) {
|
||||
writeJSON(w, http.StatusOK, Result[T]{Code: 0, Message: "OK", Data: data})
|
||||
}
|
||||
|
||||
func created[T any](w http.ResponseWriter, location string, data *T) {
|
||||
w.Header().Set("Location", location)
|
||||
writeJSON(w, http.StatusCreated, Result[T]{Code: 0, Message: "Created", Data: data})
|
||||
}
|
||||
|
||||
func fail(w http.ResponseWriter, status int, msg string) {
|
||||
writeJSON[any](w, status, Result[any]{Code: status, Message: msg, Data: nil})
|
||||
}
|
||||
Reference in New Issue
Block a user