【Golang 1.18】Let' Go, MySQL我來了…

同學們上課啦,記得當時年紀小,筆者當時流行的是網頁工程師-前/後端,但是工作非常的難找,因為Web技術已經發展20多年了,所以會的人太多了,而薪資也早已進入了大紅海時代;後來看到手機系統的出現,一不小心就轉職到了iOS打字工的行列之中;但是,工作之後發現,老舊的API寫法相當的不適合在手機端使用,要改嘛,又不是件容易的事(資深員工很難溝通?),有問題第一個顯示的也一定是在前端被發現,還是靠自己最好…

作業環境

項目 版本
CPU Apple M1
macOS Big Sur 12.3 arm64
Golang 1.18 arm64
Visual Studio Code 1.66 arm64
Postman 9.0.9 arm64
MySQL 8.0.28 arm64

事前準備

安裝

brew install go
brew install mysql

重複使用Code

package main

import (
	"fmt"
)

func main() {
	nextCountTest()
}

// 測試用
func nextCountTest() {

	nextInt := counter()

	fmt.Println(nextInt())
	fmt.Println(nextInt())
	fmt.Println(nextInt())
}

// 記數器 => 想留下來以後用
func counter() func() int {
	index := 0
	return func() int {
		index++
		return index
	}
}

Go Module

  • 為了要將Code能有效的分類處理,我們就來實作一下吧,利用『go mod init <module>』來產生『go.mod』檔案,資料夾結構如下圖…
go mod init william

  • 現在就來分類吧…
// go.mod
module william

go 1.18
// main.go
package main

import (
	"fmt"
	util "william/utility" // 以go.mod上的module為root的相對位置
)

func main() {
	nextCountTest()
}

// 計數器測試
func nextCountTest() {

	nextInt := util.Counter()

	fmt.Println(nextInt())
	fmt.Println(nextInt())
	fmt.Println(nextInt())
}
package utility

// 記數器 (大寫: publilc / 小寫: privete)
func Counter() func() int {
	index := 0
	return func() int {
		index++
		return index
	}
}
go get -u github.com/go-sql-driver/mysql		// 安裝
go get -u github.com/go-sql-driver/mysql@none	// 移除 => @none

WebAPI

Gin - Web Framework

go get -u github.com/gin-gonic/gin

建立資料庫 + 資料表

Create Database eBook;
Use eBook;
Create Table GoBook(
	id bigint(20) PRIMARY KEY AUTO_INCREMENT NOT NULL,
	isbn bigint(20),
	name char(255),
	date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Show Tables;

第一支API

package main

import (
	"net/http"

	util "william/utility" // 以go.mod上的module為root的相對位置

	"github.com/gin-gonic/gin"
	_ "github.com/go-sql-driver/mysql"
)

func main() {

	router := gin.Default()
	router.MaxMultipartMemory = 8 << 20 // 8 MiB

	userName(router)

	router.Run(util.Post)
}

// 簡單的GET測試 => http://localhost:8080/user/William/Welcome
func userName(router *gin.Engine) {

	router.GET("/user/:name/*action", func(context *gin.Context) {
		name := context.Param("name")
		action := context.Param("action")
		message := name + " is " + action
		context.String(http.StatusOK, message)
	})
}

新增資料 - Insert

// main.go
package main

import (
	"fmt"
	util "william/utility" // 以go.mod上的module為root的相對位置

	"github.com/gin-gonic/gin"
	_ "github.com/go-sql-driver/mysql"
)

func main() {

	db, error := util.ConnentDatabase()

	if error != nil {
		fmt.Println(error)
		return
	}

	router := gin.Default()
	router.MaxMultipartMemory = 8 << 20 // 8 MiB

	util.InsertBook(router, db)

	router.Run(":8080")
}
// utility/util.go
package utility

import (
	"database/sql"
	"fmt"
	"net/http"
	"time"

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

const (
	address      = "localhost"
	port         = 3306
	admin        = "root"
	password     = "12345678"
	databaseType = "mysql"
	database     = "eBook"
	table        = "GoBook"
)

// 資料庫連線 => root:12345678@tcp(localhost:3306)/eBook?charset=utf8&loc=Asia%2FShanghai&parseTime=true
func ConnentDatabase() (*sql.DB, error) {

	dataSource := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", admin, password, address, port, database)
	dataSource += "?charset=utf8&loc=Asia%2FShanghai&parseTime=true"

	database, error := sql.Open(databaseType, dataSource)

	database.SetConnMaxLifetime(time.Duration(10) * time.Second)
	database.SetMaxOpenConns(10)
	database.SetMaxIdleConns(10)

	return database, error
}

// 新增書籍 => http://localhost:8080/book/9789861079493 + form-data: name = 棋魂完全版20
func InsertBook(router *gin.Engine, db *sql.DB) {

	uri := "/book/:isbn"

	router.POST(uri, func(context *gin.Context) {

		isbn, _ := stringToInt(context.Param("isbn"))	// 取url上的參數 => context.Param()
		name := context.PostForm("name")				// 取form上的參數 => context.PostForm()

		sql := "Insert Into GoBook(isbn, name) values(?, ?)"
		result, error := db.Exec(sql, isbn, name)

		contextJSON(context, http.StatusOK, result, error)
	})
}
// utility/tools.go
package utility

import (
	"strconv"

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

// 文字 => 數字
func stringToInt(str string) (int, error) {
	return strconv.Atoi(str)
}

// 輸出JSON
func contextJSON(context *gin.Context, httpStatus int, result interface{}, error error) {

	context.JSON(httpStatus, gin.H{
		"error":  error,
		"result": result,
	})
}

搜尋資料 - Select

// 建立GoBook的資料結構
type GoBook struct {
	ID         int       `json:"id"`   // <欄位> <類型> <真實在json輸出的欄位名稱>
	ISBN       int       `json:"isbn"` // ISBN是數字,在json叫isbn
	Name       string    `json:"name"`
	CreateTime time.Time `json:"date"`
}

// <Get>搜尋書籍 => http://localhost:8080/book
func SelectBooks(router *gin.Engine, db *sql.DB) {

	uri := "/book"

	router.GET(uri, func(context *gin.Context) {
		
		books := []GoBook{}
		sql := "Select * From GoBook"
		rows, error := db.Query(sql)

		defer func() {
			rows.Close()
		}()

		for rows.Next() {
			var book GoBook
			rows.Scan(&book.ID, &book.ISBN, &book.Name, &book.CreateTime) // 要注意的是使用指標
			books = append(books, book)
		}

		contextJSON(context, http.StatusOK, books, error)
	})
}

更新資料 - Select

// <PUT>修改某一本書籍 => http://localhost:8080/book/9789861079493
func UpdateBook(router *gin.Engine, db *sql.DB) {

	uri := "/book/:isbn"

	router.PUT(uri, func(context *gin.Context) {

		isbn, _ := stringToInt(context.Param("isbn"))      // 取url上的參數 => context.Param()
		rawJSON, _ := ioutil.ReadAll(context.Request.Body) // 讀取RawBody上的值
		sql := fmt.Sprintf("Update %s Set name=? Where isbn=?", table)

		dictionary := RawJSONToMap(rawJSON)
		name := dictionary["name"]

		result, error := db.Exec(sql, name, isbn)
		contextJSON(context, http.StatusOK, result, error)
	})
}

刪除資料 - Delete

// <Delete>刪除某一本書籍 => http://localhost:8080/book/9789861079493
func DeleteBook(router *gin.Engine, db *sql.DB) {

	uri := "/book/:isbn"

	router.DELETE(uri, func(context *gin.Context) {

		var error error = nil
		var count int64 = 0
		var isSuccess bool = false

		defer func() {
			result := map[string]interface{}{"isSuccess": isSuccess, "count": count}
			contextJSON(context, http.StatusOK, result, error)
		}()

		isbn, error := stringToInt(context.Param("isbn"))
		if error != nil { return }

		sql := fmt.Sprintf("Delete From %s Where isbn=?", table)
		result, error := db.Exec(sql, isbn)
		if error != nil { return }

		rowsaffected, error := result.RowsAffected() // 很嚴謹的寫法會判斷RowsAffected()是否與處理的資料筆數一致
		if error != nil { return }

		isSuccess = true
		count = rowsaffected
	})
}

檔案上傳 / 下載

// 檔案上傳 => multipart/form-data, default is 32 MiB / file=<base64>
func UploadFile(router *gin.Engine, db *sql.DB) {

	key := "file"
	folder := "/Users/william.weng/Desktop/"

	router.POST("/upload", func(context *gin.Context) {

		var error error = nil
		var isSuccess bool = false
		var filename = uuid.NewString() + ".jpg"

		defer func() {
			result := map[string]interface{}{"isSuccess": isSuccess}
			contextJSON(context, http.StatusOK, result, error)
		}()

		filePath := folder + filename
		file, error := context.FormFile(key)
		if error != nil { return }

		error = context.SaveUploadedFile(file, filePath)
		if error != nil { return }

		isSuccess = true
	})
}
func main() {

	router := gin.Default()
	router.MaxMultipartMemory = 8 << 20 // 8 MiB

	router.Static("static", "./static")	// http://localhost:8080/static <=> ./static
	router.Run(":8080")
}

iOS 推播功能

go get -u github.com/sideshow/apns2
// iOS推播功能
func pushNotification(context *gin.Context, deviceToken string) (*apns2.Response, error) {

	const authKeyP8 = "./<AuthKey>.p8"
	const keyId = "<KeyId>"
	const teamID = "<TeamID>"
	const topic = "idv.william.Example"

	var authKey *ecdsa.PrivateKey = nil
	var response *apns2.Response = nil
	var error error = nil

	payload := []byte(`{"aps":{"alert":"Golang,愛你喲…"}}`)
	authKey, error = token.AuthKeyFromFile(authKeyP8)

	if error != nil {
		return response, error
	}

	notification := &apns2.Notification{}
	notification.Topic = topic
	notification.DeviceToken = deviceToken
	notification.Payload = payload

	token := &token.Token{
		AuthKey: authKey,
		KeyID:   keyId,
		TeamID:  teamID,
	}

	client := apns2.NewTokenClient(token).Development()
	response, error = client.Push(notification)

	if error != nil {
		return response, error
	}

	return response, error
}

Android 推播功能

// Android推播功能
func firebaseCloudMessaging(token string) (*fcm.Response, error) {

	apiKey := "<YourProductApiKey>"

	message := &fcm.Message{
		To: token,
		Data: map[string]interface{}{
			"foo": "bar",
		},
		Notification: &fcm.Notification{
			Title: "Golang",
			Body:  "Golang,愛你喲…",
		},
	}

	client, error := fcm.NewClient(apiKey)

	if error != nil {
		return nil, error
	}

	return client.Send(message)
}

範例程式碼下載

後記