20240324-learn-go
quickstart
package main import ( "fmt" ) func main() { fmt.Println("Hello, world!") }
編譯可執行檔案:
go build hello.go
go run hello.go
go mod init alanhc/school
go install OOO
go.mod
Tutorial: Get started with Go
- dependency
- how?
go mod init example/hello
- Hello world
package main import "fmt" func main() { fmt.Println("Hello, World!") }
go run .
- external package
- e.g. https://pkg.go.dev/search?q=quote
- :新增或移除未使用的pkg ,會產生go.sum(用於驗證)
go mod tidy
import "rsc.io/quote" func main() { fmt.Println(quote.Go())
Tutorial: Create a Go module
Call your code from another module
- production
- 會公開 example.com/greetings
- 替換規則到指定路徑
go mod edit -replace example.com/greetings=../greetings
- 在hello/hello.go
go run .
Return and handle an error
import ( "errors" func Hello(name string) (string, error) { // If no name was given, return an error with a message. if name == "" { return "", errors.New("empty name") } return message, nil // nil 代表沒有錯誤
package main import ( "fmt" "log" "example.com/greetings" ) func main() { // Set properties of the predefined Logger, including // the log entry prefix and a flag to disable printing // the time, source file, and line number. log.SetPrefix("greetings: ") // log 開頭會是greetings log.SetFlags(0) //禁用列印時間 // Request a greeting message. message, err := greetings.Hello("") // If an error was returned, print it to the console and // exit the program. if err != nil { log.Fatal(err) } // If no error was returned, print the returned message // to the console. fmt.Println(message) }
Return a random greeting
greetings.go
import ( "errors" "fmt" "math/rand" ... func Hello(name string) (string, error) { ... message := fmt.Sprintf(randomFormat(), name) // randomFormat returns one of a set of greeting messages. The returned // message is selected at random. func randomFormat() string { // A slice of message formats. formats := []string{ "Hi, %v. Welcome!", "Great to see you, %v!", "Hail, %v! Well met!", } // Return a randomly selected message format by specifying // a random index for the slice of formats. return formats[rand.Intn(len(formats))] }
hello.go
message, err := greetings.Hello("Gladys")
Return greetings for multiple people
greeting.go
// Hellos returns a map that associates each of the named people // with a greeting message. func Hellos(names []string) (map[string]string, error) { // A map to associate names with messages. messages := make(map[string]string) // Loop through the received slice of names, calling // the Hello function to get a message for each name. for _, name := range names { message, err := Hello(name) if err != nil { return nil, err } // In the map, associate the retrieved message with // the name. messages[name] = message } return messages, nil }
hello.go
// A slice of names. names := []string{"Gladys", "Samantha", "Darrin"} // Request greeting messages for the names. messages, err := greetings.Hellos(names) if err != nil { log.Fatal(err) } // If no error was returned, print the returned map of // messages to the console. fmt.Println(messages)
Add a test
- test: 直接在原本資料夾新增 e.g.
_test.go
greetings_test.go
- testing.T: 指到testing pkg的指標
Compile and install the application
- 會產生 .hello / hello.exe
go build
- 查詢安裝路徑:
go list -f '{{.Target}}'
- 確認GOPATH要加到 ~/.zshrc
Tutorial: Getting started with multi-module workspaces
- 新增workspace:
go work init ./hello
- 執行:
go run ./hello
- 使用外部
git clone https://go.googlesource.com/example
- 加到workspace
go work use ./example/hello
- 新增到workspace: 新增int.go
workspace/example/hello/reverse
- 執行
go run ./hello
package main import ( "fmt" "golang.org/x/example/hello/reverse" ) func main() { fmt.Println(reverse.String("Hello"), reverse.Int(24601)) }
Tutorial: Accessing a relational database
mkdir data-access
go mod init example/data-access
mysql -u root -p
mysql> create database recordings;
mysql> use recordings; Database changed
create-tables.sql
DROP TABLE IF EXISTS album; CREATE TABLE album ( id INT AUTO_INCREMENT NOT NULL, title VARCHAR(128) NOT NULL, artist VARCHAR(255) NOT NULL, price DECIMAL(5,2) NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO album (title, artist, price) VALUES ('Blue Train', 'John Coltrane', 56.99), ('Giant Steps', 'John Coltrane', 63.99), ('Jeru', 'Gerry Mulligan', 17.99), ('Sarah Vaughan', 'Sarah Vaughan', 34.98);
mysql> source /path/to/create-tables.sql
- main.go
package main import ( "database/sql" "fmt" "log" "os" "github.com/go-sql-driver/mysql" ) var db *sql.DB type Album struct { ID int64 Title string Artist string Price float32 } func main() { // Capture connection properties. cfg := mysql.Config{ User: os.Getenv("DBUSER"), Passwd: os.Getenv("DBPASS"), Net: "tcp", Addr: "127.0.0.1:3306", DBName: "recordings", } // Get a database handle. var err error db, err = sql.Open("mysql", cfg.FormatDSN()) if err != nil { log.Fatal(err) } pingErr := db.Ping() if pingErr != nil { log.Fatal(pingErr) } fmt.Println("Connected!") albums, err := albumsByArtist("John Coltrane") if err != nil { log.Fatal(err) } fmt.Printf("Albums found: %v\n", albums) // Hard-code ID 2 here to test the query. alb, err := albumByID(2) if err != nil { log.Fatal(err) } fmt.Printf("Album found: %v\n", alb) albID, err := addAlbum(Album{ Title: "The Modern Sound of Betty Carter", Artist: "Betty Carter", Price: 49.99, }) if err != nil { log.Fatal(err) } fmt.Printf("ID of added album: %v\n", albID) } // albumsByArtist queries for albums that have the specified artist name. func albumsByArtist(name string) ([]Album, error) { // An albums slice to hold data from returned rows. var albums []Album rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name) if err != nil { return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) } defer rows.Close() // Loop through rows, using Scan to assign column data to struct fields. for rows.Next() { var alb Album if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil { return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) } albums = append(albums, alb) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) } return albums, nil } // albumByID queries for the album with the specified ID. func albumByID(id int64) (Album, error) { // An album to hold data from the returned row. var alb Album row := db.QueryRow("SELECT * FROM album WHERE id = ?", id) if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil { if err == sql.ErrNoRows { return alb, fmt.Errorf("albumsById %d: no such album", id) } return alb, fmt.Errorf("albumsById %d: %v", id, err) } return alb, nil } // addAlbum adds the specified album to the database, // returning the album ID of the new entry func addAlbum(alb Album) (int64, error) { result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price) if err != nil { return 0, fmt.Errorf("addAlbum: %v", err) } id, err := result.LastInsertId() if err != nil { return 0, fmt.Errorf("addAlbum: %v", err) } return id, nil }
Tutorial: Developing a RESTful API with Go and Gin
- Design API endpoints
/albums
/albums/:id
- c 是一個指向 gin.Context 結構體的指標,優點:
c *gin.Context
- 傳址效率更高,可降低內存使用,因為傳遞一個大型結構體時,傳值會複製該結構體的每一個欄位,這樣會消耗更多的記憶體和 CPU 資源
Tutorial: Getting started with generics
- 寫法:放在 裡
[]
- 限制型別(Constraints)
func SumNumbers[T int | float64](a, b T) T { return a + b }
package main import "fmt" type Number interface { int64 | float64 } func main() { // Initialize a map for the integer values ints := map[string]int64{ "first": 34, "second": 12, } // Initialize a map for the float values floats := map[string]float64{ "first": 35.98, "second": 26.99, } fmt.Printf("Non-Generic Sums: %v and %v\n", SumInts(ints), SumFloats(floats)) fmt.Printf("Generic Sums: %v and %v\n", SumIntsOrFloats[string, int64](ints), SumIntsOrFloats[string, float64](floats)) fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n", SumIntsOrFloats(ints), SumIntsOrFloats(floats)) fmt.Printf("Generic Sums with Constraint: %v and %v\n", SumNumbers(ints), SumNumbers(floats)) } // SumInts adds together the values of m. func SumInts(m map[string]int64) int64 { var s int64 for _, v := range m { s += v } return s } // SumFloats adds together the values of m. func SumFloats(m map[string]float64) float64 { var s float64 for _, v := range m { s += v } return s } // SumIntsOrFloats sums the values of map m. It supports both floats and integers // as map values. func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { var s V for _, v := range m { s += v } return s } // SumNumbers sums the values of map m. Its supports both integers // and floats as map values. func SumNumbers[K comparable, V Number](m map[K]V) V { var s V for _, v := range m { s += v } return s }
Q&A
- Gin 框架中,使用 Context 這個名稱是為了表示這個物件與當前的 HTTP 請求的「上下文」有關。Context 一詞表示這個物件包含了有關請求處理的所有相關信息和操作,讓開發者可以在處理請求的過程中方便地存取和操作請求及回應。
- why? 1. 什麼是 context? Go 的 context 是一個用來在同一請求的不同協程間傳遞訊息的機制,它能夠:
- 取消操作:當一個操作不再需要進行時,透過 context 可以通知相關的協程停止運行。
- 設置超時:允許設置一個超時時間,超過時間後自動取消操作。
- 傳遞元數據:可以在請求處理過程中,跨不同的協程傳遞一些與請求相關的數據(如用戶 ID、認證資訊等)。 2. context 的用途 在需要並發處理的場景,特別是當有長時間運行的任務時,context 非常有用。例如: • 處理 HTTP 請求時,每個請求可能會啟動多個協程來執行子任務,而如果請求被取消了,這些子任務也需要立刻停止執行。 • 操作數據庫查詢或外部 API 時,為了避免請求卡死,通常會設置一個超時,context 可以幫助實現這種超時控制。 3. 常見的 context 用法 Go 中的 context 有幾個常見的使用模式: • 背景上下文 (context.Background()): • 這是用來初始化 context 的根節點,它表示一個空的上下文,通常作為 context 的起點。
ctx := context.Background()
- 帶取消功能的上下文 (context.WithCancel()):
- 這會創建一個新的 context,並且可以使用 cancel 函數來手動取消這個上下文。
ctx, cancel := context.WithCancel(context.Background()) // 需要時調用 cancel() 來取消此上下文 cancel()
- 帶超時功能的上下文 (context.WithTimeout()):
- 用來設置一個操作的超時時間,超過時間自動取消操作。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 超過 5 秒自動取消
- 帶截止時間的上下文 (context.WithDeadline()):
- 類似於 WithTimeout(),但直接指定一個具體的時間點。
deadline := time.Now().Add(1 * time.Hour) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel()
- 傳遞資料 (context.WithValue()):
- 可以將一些關聯數據傳遞到 context 中,例如用戶 ID 等,在請求處理過程中不同協程可以共享這些數據。
ctx := context.WithValue(context.Background(), "userID", 123) userID := ctx.Value("userID")
- 4. context 的應用場景
- HTTP 請求處理: 當處理一個 HTTP 請求時,如果該請求被取消或超時,我們可以通過 context 來通知所有處理該請求的協程停止操作。
- 長時間運行的任務: 在進行一些需要長時間運行的任務時,context 可以幫助控制任務的生命週期,確保在適當的時候可以中止任務,節省資源。 • 並發操作: 當我們在多個協程中進行併發操作時,context 可以用來協調這些協程的執行狀態,比如某個協程完成或取消時,通知其他協程一起停止。
package main import ( "net/http" "github.com/gin-gonic/gin" ) // album represents data about a record album. type album struct { ID string `json:"id"` Title string `json:"title"` Artist string `json:"artist"` Price float64 `json:"price"` } // albums slice to seed record album data. var albums = []album{ {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}, {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99}, {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99}, } func main() { router := gin.Default() router.GET("/albums", getAlbums) router.GET("/albums/:id", getAlbumByID) router.POST("/albums", postAlbums) router.Run("localhost:8080") } // getAlbums responds with the list of all albums as JSON. func getAlbums(c *gin.Context) { c.IndentedJSON(http.StatusOK, albums) } // postAlbums adds an album from JSON received in the request body. func postAlbums(c *gin.Context) { var newAlbum album // Call BindJSON to bind the received JSON to // newAlbum. if err := c.BindJSON(&newAlbum); err != nil { return } // Add the new album to the slice. albums = append(albums, newAlbum) c.IndentedJSON(http.StatusCreated, newAlbum) } // getAlbumByID locates the album whose ID value matches the id // parameter sent by the client, then returns that album as a response. func getAlbumByID(c *gin.Context) { id := c.Param("id") // Loop through the list of albums, looking for // an album whose ID value matches the parameter. for _, a := range albums { if a.ID == id { c.IndentedJSON(http.StatusOK, a) return } } c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"}) }
web
Routing (mux)
Middleware
JSON
Websocket
crypto / password hashing
gRPC
install
Quickstart
Basic
- https://grpc.io/docs/languages/go/basics/
- Define a service in a file.
.proto
- Generate server and client code using the protocol buffer compiler.
- Use the Go gRPC API to write a simple client and server for your service.
- why gRPC?
- 寫proto file
- 產生
protoc --go_out=. --go_opt=paths=source_relative
--go-grpc_out=. --go-grpc_opt=paths=source_relative proto/rating.proto
proto file import 問題
solution:
protoc -I=. --go_out=$GOROOT/src --go-grpc_out=require_unimplemented_servers=false:$GOROOT/src proto/**.proto
protoc -I=. --go_out=$GOROOT/src --go-grpc_out=$GOROOT/src proto/rating.proto
Mongodb
go get go.mongodb.org/mongo-driver/mongo
Mongodb grpc go
syntax = "proto3"; option go_package = "alanhc/ratingpb"; message Rating { // 教師姓名 string teacher = 1; // 科目名稱 string subject = 2; // 顯示名稱 string name = 3; // 評分內容 string content = 4; // 建立時間 string created_at = 5; // 修改時間 string modified_at = 6; // 作業評分 float rate_homework = 9; // 學習評分 float rate_learning = 10; // 推薦評分 float rate_recommendation = 11; } message ListRatingReq { string teacher = 1; string subject = 2; } message ListRatingRes { Rating rating = 1; } service ratingService { rpc ListRating(ListRatingReq) returns (stream ListRatingRes); }
package main import ( "context" "fmt" "log" "net" "os" "os/signal" pb "alanhc/ratingpb" "github.com/joho/godotenv" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "google.golang.org/grpc" ) var db *mongo.Database type RatingItem struct { Teacher string `bson:"teacher"` Subject string `bson:"subject"` Name string `bson:"name"` Content string `bson:"content"` CreatedAt string `bson:"createdAt"` ModifiedAt string `bson:"modifiedAt"` RateHomework float32 `bson:"rateHomework"` RateLearning float32 `bson:"rateLearning"` RateRecommendation float32 `bson:"rateRecommendation"` } func (s *RatingServiceServer) ListRating(req *pb.ListRatingReq, stream pb.RatingService_ListRatingServer) error { fmt.Println("ReadRating function was invoked with ", req.Teacher, req.Subject) cursor, err := db.Collection("rating").Find(context.Background(), bson.M{"teacher": req.Teacher, "subject": req.Subject}) if err != nil { log.Fatalf("Error finding rating: %v", err) } defer cursor.Close(context.Background()) data := &RatingItem{} for cursor.Next(context.Background()) { // var data pb.Rating err := cursor.Decode(data) if err != nil { log.Fatalf("Error decoding data: %v", err) } // fmt.Println(data) // ratingPb := data.toRatingPB() if err := stream.Send(&pb.ListRatingRes{ Rating: &pb.Rating{ Teacher: data.Teacher, Subject: data.Subject, Name: data.Name, Content: data.Content, CreatedAt: data.CreatedAt, ModifiedAt: data.ModifiedAt, RateHomework: data.RateHomework, RateLearning: data.RateLearning, RateRecommendation: data.RateRecommendation, }, }); err != nil { log.Fatalf("Error sending data to client: %v", err) } } err = cursor.Close(context.Background()) if err := cursor.Err(); err != nil { log.Fatalf("Error cursor.Err(): %v", err) } return nil } type RatingServiceServer struct { } // ListRating implements ratingpb.RatingServiceServer. // func (s *RatingServiceServer) ListRating(req *pb.ListRatingReq, stream pb.RatingService_ListRatingServer) error { // fmt.Println("ReadRating function was invoked with ", req.Teacher, req.Subject) // cursor, err := db.Collection("rating").Find(context.Background(), bson.M{"teacher": req.Teacher, "subject": req.Subject}) // if err != nil { // log.Fatalf("Error finding rating: %v", err) // } // defer cursor.Close(context.Background()) // data := &RatingItem{} // for cursor.Next(context.Background()) { // // var data pb.Rating // err := cursor.Decode(data) // if err != nil { // log.Fatalf("Error decoding data: %v", err) // } // // fmt.Println(data) // // ratingPb := data.toRatingPB() // if err := (*stream).Send(&pb.ListRatingRes{ // Fix: Dereference the pointer to the interface before calling Send method // Rating: &pb.Rating{ // Teacher: data.Teacher, // Subject: data.Subject, // Name: data.Name, // Content: data.Content, // CreatedAt: data.CreatedAt, // ModifiedAt: data.ModifiedAt, // RateHomework: data.RateHomework, // RateLearning: data.RateLearning, // RateRecommendation: data.RateRecommendation, // }, // }); err != nil { // log.Fatalf("Error sending data to client: %v", err) // } // //stream.Send(data) // } // err = cursor.Close(context.Background()) // if err := cursor.Err(); err != nil { // log.Fatalf("Error cursor.Err(): %v", err) // } // return nil // } // ListRating implements ratingpb.RatingServiceServer. // func (s *RatingServiceServer) ListRating(ctx context.Context, req *pb.ListRatingReq, stream *pb.RatingService_ListRatingServer) error { // fmt.Println("ReadRating function was invoked with ", req.Teacher, req.Subject) // cursor, err := db.Collection("rating").Find(context.Background(), bson.M{"teacher": req.Teacher, "subject": req.Subject}) // if err != nil { // log.Fatalf("Error finding rating: %v", err) // } // defer cursor.Close(context.Background()) // data := &RatingItem{} // for cursor.Next(context.Background()) { // // var data pb.Rating // err := cursor.Decode(data) // if err != nil { // log.Fatalf("Error decoding data: %v", err) // } // // fmt.Println(data) // // ratingPb := data.toRatingPB() // if err := (*stream).Send(&pb.ListRatingRes{ // Fix: Dereference the pointer to the interface before calling Send method // Rating: &pb.Rating{ // Teacher: data.Teacher, // Subject: data.Subject, // Name: data.Name, // Content: data.Content, // CreatedAt: data.CreatedAt, // ModifiedAt: data.ModifiedAt, // RateHomework: data.RateHomework, // RateLearning: data.RateLearning, // RateRecommendation: data.RateRecommendation, // }, // }); err != nil { // log.Fatalf("Error sending data to client: %v", err) // } // //stream.Send(data) // } // err = cursor.Close(context.Background()) // if err := cursor.Err(); err != nil { // log.Fatalf("Error cursor.Err(): %v", err) // } // return nil // } var mongoCtx context.Context func main() { err := godotenv.Load() if err != nil { log.Fatal("Error loading .env file:", err) } CONNECTION_STRING := os.Getenv("CONNECTION_STRING") // fmt.Println(CONNECTION_STRING) fmt.Println("Starting server on port :50051...") lis, err := net.Listen("tcp", "localhost:50051") if err != nil { log.Fatalf("failed to listen: %v", err) } var grpc_opts []grpc.ServerOption srv := &RatingServiceServer{} // var srv *RatingServiceServer grpcServer := grpc.NewServer(grpc_opts...) pb.RegisterRatingServiceServer(grpcServer, srv) // Initialize MongoDb client fmt.Println("Connecting to MongoDB...") mongoCtx = context.Background() serverAPI := options.ServerAPI(options.ServerAPIVersion1) opts := options.Client().ApplyURI(CONNECTION_STRING).SetServerAPIOptions(serverAPI) // Create a new client and connect to the server client, err := mongo.Connect(context.TODO(), opts) // 連線到 MongoDB if err != nil { panic(err) } err = client.Ping(mongoCtx, nil) if err != nil { log.Fatalf("Could not connect to MongoDB: %v\n", err) } else { fmt.Println("Connected to Mongodb") } db = client.Database("school") defer func() { if err = client.Disconnect(context.TODO()); err != nil { panic(err) } }() // Send a ping to confirm a successful connection var result bson.M if err := client.Database("admin").RunCommand(context.TODO(), bson.D{{"ping", 1}}).Decode(&result); err != nil { panic(err) } fmt.Println("Pinged your deployment. You successfully connected to MongoDB!") fmt.Println("Server is running on port 50051") defer lis.Close() // Start the server in a child routine go func() { if err := grpcServer.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }() fmt.Println("Server succesfully started on port :50051") // Create a channel to receive OS signals c := make(chan os.Signal) // Relay os.Interrupt to our channel (os.Interrupt = CTRL+C) // Ignore other incoming signals signal.Notify(c, os.Interrupt) // Block main routine until a signal is received // As long as user doesn't press CTRL+C a message is not passed // And our main routine keeps running // If the main routine were to shutdown so would the child routine that is Serving the server <-c fmt.Println("\nStopping the server...") grpcServer.Stop() lis.Close() fmt.Println("Closing MongoDB connection") // db.Disconnect(mongoCtx) fmt.Println("Done.") }
CLI
- Cobra CLI
- https://github.com/spf13/cobra
(待看) Deploy Go Apps on Google Cloud Serverless Platforms
教學網站
Ref
sample code
CRUD example - http + pg
package main import ( "database/sql" "fmt" "log" "net/http" _ "github.com/lib/pq" ) type Student struct { ID int Name string Age int } func main() { db, err := sql.Open("postgres", "user=postgres dbname=school sslmode=disable") if err != nil { log.Fatal(err) } defer db.Close() // 確保程式結束前關閉資料庫連線 // 建立路由 http.HandleFunc("/hello", hello) http.HandleFunc("/students", listStudents(db)) http.HandleFunc("/students/create", createStudent(db)) http.HandleFunc("/students/update", updateStudent(db)) http.HandleFunc("/students/delete", deleteStudent(db)) log.Fatal(http.ListenAndServe(":8080", nil)) // 如果錯誤就記錄錯誤 } func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello, World!") } func listStudents(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { rows, err := db.Query("SELECT * FROM students") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer rows.Close() for rows.Next() { var s Student if err := rows.Scan(&s.ID, &s.Name, &s.Age); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprintf(w, "%d %s %d\n", s.ID, s.Name, s.Age) } } } func createStudent(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { r.ParseForm() name := r.FormValue("name") age := r.FormValue("age") _, err := db.Exec("INSERT INTO students (name, age) VALUES ($1, $2)", name, age) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprint(w, "created") } } func updateStudent(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { r.ParseForm() id := r.FormValue("id") name := r.FormValue("name") age := r.FormValue("age") _, err := db.Exec("UPDATE students SET name=$1, age=$2 WHERE id=$3", name, age, id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprint(w, "updated") } } func deleteStudent(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { r.ParseForm() id := r.FormValue("id") _, err := db.Exec("DELETE FROM students WHERE id=$1", id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprint(w, "deleted") } }