代码仓库
Go语言,也称作Golang,是由Google开发的一种静态强类型、编译型语言,具有垃圾回收功能。它在2009年公开发布,由Robert Griesemer、Rob Pike和Ken Thompson设计。Go语言的设计目标是为了解决大型软件系统的构建问题,特别是在Google内部,这些系统需要高效的编译、高效的执行以及高效的代码维护。
Go的主要特点包括:
Go语言适用于各种类型的项目,从小型个人项目到大型分布式系统。在本书中,我们将使用Go语言构建一个博客系统,这将帮助我们理解Go语言在实际应用中的强大功能。
让我们开始安装Go语言。请访问Go语言官方网站(https://golang.org/dl/)下载适合您操作系统的安装包。下载完成后,请按照官方指南完成安装。
安装Go后,您可以打开终端或命令提示符并运行以下命令来验证安装:
go version
这应该会显示安装的Go版本。例如:
go version go1.15.6 linux/amd64
接下来,设置您的工作空间。Go语言的工作空间是存放Go代码的地方。它有一个特定的目录结构:
src
目录包含Go的源文件,pkg
目录包含包对象,bin
目录包含可执行文件。您可以通过设置环境变量GOPATH
来指定您的工作空间目录。例如,在Unix系统上:
export GOPATH=$HOME/go
在Windows系统上:
set GOPATH=c:\go
编写Hello World程序是学习新编程语言的传统。在Go中,这个程序看起来是这样的:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
将上面的代码保存为hello.go
。然后在命令行中运行以下命令来编译并运行程序:
go run hello.go
如果一切顺利,您将看到终端打印出“Hello, World!”。
Go程序由包(packages)组成。每个Go文件都属于一个包,且文件的第一行声明了它所属的包。main
包是特殊的,它告诉Go编译器这个程序是可执行的,而不是一个库。
在main
包中,main
函数也是特殊的——它是程序执行的入口点。在上面的Hello World程序中,我们导入了fmt
包,这是一个包含I/O函数的标准库包。我们使用fmt.Println
来输出字符串到标准输出。
现在是时候动手写代码了。作为练习,请尝试以下操作:
这是一个接收用户输入的示例程序:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your name: ")
name, _ := reader.ReadString('\n')
fmt.Printf("Hello, %s", name)
}
这个程序使用bufio
包创建一个新的缓冲读取器,用于读取来自标准输入的数据。它提示用户输入名字,然后读取输入并存储在变量name
中,最后使用fmt.Printf
打印个性化的问候语。
通过完成第一章,读者应该能够理解Go语言的基础,安装并设置好Go开发环境,并编写、运行简单的Go程序。下一章将深入探讨Go语言的核心概念和功能。
在Go语言中,变量是存储程序执行过程中数据的容器。Go是静态类型语言,这意味着变量是有明确定义的类型,类型在编译时就已确定,并且类型在整个程序运行期间不会改变。
var message string
message = "Hello, Go!"
// 或者一步到位
var greeting = "Hello, Go!"
// 短变量声明,最常用
name := "World"
Go语言中的基本数据类型包括:
控制结构在Go语言中用于控制程序的执行流程。Go提供了多种控制结构,例如if语句、for循环和switch语句。
if number := 10; number%2 == 0 {
fmt.Println(number, "is even")
} else {
fmt.Println(number, "is odd")
}
// 标准的for循环
for i := 0; i < 5; i++ {
fmt.Println("Value of i is:", i)
}
// 类似while的for循环
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println("Sum is:", sum)
dayOfWeek := 3
switch dayOfWeek {
case 1:
fmt.Println("Monday")
case 2:
fmt.Println("Tuesday")
case 3:
fmt.Println("Wednesday")
// ...
default:
fmt.Println("Invalid day")
}
函数是执行特定任务的代码块。在Go中,您可以定义带有参数和返回值的函数。
func add(x int, y int) int {
return x + y
}
// 当连续两个或多个参数的类型相同时,我们可以仅声明最后一个参数的类型
func subtract(x, y int) int {
return x - y
}
result1 := add(6, 7)
result2 := subtract(10, 3)
fmt.Println("Addition result:", result1)
fmt.Println("Subtraction result:", result2)
在Go中,错误处理是通过返回一个错误类型的值来完成的。如果一个函数可能产生错误,它通常是函数返回值列表中的最后一个。
func divide(x, y float64) (float64, error) {
if y == 0.0 {
return 0.0, errors.New("cannot divide by zero")
}
return x / y, nil
}
result, err := divide(10.0, 0.0)
if err != nil {
log.Fatal(err)
}
fmt.Println("Result:", result)
作为练习,尝试以下操作:
func concatenate(str1, str2 string) string {
return str1 + str2
}
fmt.Println(concatenate("Hello, ", "Go!"))
func sum(numbers []int) int {
total := 0
for _, number := range numbers {
total += number
}
return total
}
fmt.Println(sum([]int{1, 2, 3, 4, 5}))
func factorial(n int) int {
if n == 0 {
return 1
}
return n * factorial(n-1)
}
fmt.Println(factorial(5))
通过完成第二章,读者应该能够理解Go语言的变量、数据类型、控制结构、函数定义以及错误处理的基础知识。这些是构建更复杂程序的基石。在下一章中,我们将探索Go的标准库,它提供了大量方便的工具和函数,可以帮助我们更快地开发程序。
Go的标准库是一组广泛的包,提供了从输入/输出处理到网络编程的功能。在这一章节中,我们将探索一些对于构建命令行博客系统非常有用的标准库包。
fmt
包fmt
包实现了格式化的I/O函数,类似于C语言的printf和scanf。我们已经在前面的Hello World程序中使用了fmt.Println
来输出文本。
name := "Go Programmer"
age := 30
fmt.Printf("My name is %s and I am %d years old.\n", name, age)
var input string
fmt.Print("Enter your input: ")
fmt.Scanln(&input)
fmt.Println("You entered:", input)
io/ioutil
和os
包文件操作是大多数程序中的常见任务。在Go中,io/ioutil
和os
包提供了这方面的功能。
content, err := ioutil.ReadFile("blogpost.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println("File content:", string(content))
message := []byte("Hello, Go Blog!")
err := ioutil.WriteFile("blogpost.txt", message, 0644)
if err != nil {
log.Fatal(err)
}
if _, err := os.Stat("blogpost.txt"); os.IsNotExist(err) {
fmt.Println("The file does not exist.")
} else {
fmt.Println("The file exists.")
}
time
包处理日期和时间是编程中的一个常见需求。Go的time
包提供了这方面的功能。
now := time.Now()
fmt.Println("Current time:", now)
fmt.Println("Formatted time:", now.Format("2006-01-02 15:04:05"))
past := time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC)
duration := now.Sub(past)
fmt.Println("Duration since past:", duration)
encoding/json
包JSON是一种轻量级的数据交换格式,经常被用于网络通信。在Go中,encoding/json
包提供了JSON数据的编码和解码功能。
type BlogPost struct {
Title string
Content string
Author string
Views int
}
post := BlogPost{
Title: "Exploring Go's Standard Library",
Content: "Go's standard library is vast...",
Author: "Jane Doe",
Views: 3490,
}
jsonBytes, err := json.Marshal(post)
if err != nil {
log.Fatal(err)
}
fmt.Println("JSON encoding:", string(jsonBytes))
var post BlogPost
err := json.Unmarshal(jsonBytes, &post)
if err != nil {
log.Fatal(err)
}
fmt.Println("Blog post:", post)
作为练习,尝试以下操作:
type BlogPost struct {
Title string `json:"title"`
Content string `json:"content"`
Author string `json:"author"`
Date time.Time `json:"date"`
}
func encodeToJSON(post BlogPost) ([]byte, error) {
return json.Marshal(post)
}
func decodeFromJSON(jsonBytes []byte) (BlogPost, error) {
var post BlogPost
err := json.Unmarshal(jsonBytes, &post)
return post, err
}
func savePostToFile(filename string, post BlogPost) error {
jsonBytes, err := encodeToJSON(post)
if err != nil {
return err
}
return ioutil.WriteFile(filename, jsonBytes, 0644)
}
func loadPostFromFile(filename string) (BlogPost, error) {
jsonBytes, err := ioutil.ReadFile(filename)
if err != nil {
return BlogPost{}, err
}
return decodeFromJSON(jsonBytes)
}
// 使用上述函数
post := BlogPost{
Title: "My First Blog Post",
Content: "Content of my first blog post",
Author: "John Doe",
Date: time.Now(),
}
filename := "post.json"
// 保存博客文章到文件
err := savePostToFile(filename, post)
if err != nil {
log.Fatal(err)
}
// 从文件中读取博客文章
loadedPost, err := loadPostFromFile(filename)
if err != nil {
log.Fatal(err)
}
fmt.Println("Loaded blog post:", loadedPost)
通过完成第三章,读者应该能够理解如何使用Go的标准库来进行文件操作、日期和时间处理、以及JSON的编码和解码。这些技能对于构建命令行博客系统至关重要。在下一章中,我们将学习如何使用Go的网络编程功能来让我们的博客系统可以通过网络进行数据交换。
在这一章节中,我们将介绍Go语言在网络编程方面的能力。Go的net
包提供了丰富的网络编程功能,包括TCP/UDP协议、HTTP客户端和服务端的实现等。
TCP(传输控制协议)是一种可靠的、面向连接的协议。下面的例子演示了如何创建一个简单的TCP服务器,它监听本地端口,并回显接收到的消息。
package main
import (
"bufio"
"fmt"
"net"
"os"
)
func main() {
// 监听本地的12345端口
listener, err := net.Listen("tcp", "localhost:12345")
if err != nil {
fmt.Println("Error listening:", err.Error())
os.Exit(1)
}
defer listener.Close()
fmt.Println("Listening on localhost:12345")
for {
// 等待连接
conn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting:", err.Error())
os.Exit(1)
}
fmt.Println("Received connection")
// 处理连接
go handleRequest(conn)
}
}
// 处理请求
func handleRequest(conn net.Conn) {
defer conn.Close()
// 创建一个新的reader,从TCP连接读取数据
reader := bufio.NewReader(conn)
for {
// 读取客户端发送的数据
message, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Error reading:", err.Error())
break
}
fmt.Print("Message received: ", string(message))
// 回显消息
conn.Write([]byte(message))
}
}
Go的net/http
包让创建HTTP服务器变得非常简单。下面的代码展示了如何创建一个基本的HTTP服务器,它可以响应GET请求。
package main
import (
"fmt"
"net/http"
)
func main() {
// 设置路由和处理函数
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to the Go Blog!")
})
// 监听并在8080端口启动服务器
fmt.Println("Server is listening on port 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("Error starting server:", err)
return
}
}
Go的net/http
包不仅可以创建服务器,还可以作为客户端发送请求。以下示例展示了如何发送GET请求并读取响应。
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
// 向服务器发送GET请求
response, err := http.Get("http://example.com")
if err != nil {
fmt.Println("Error making GET request:", err)
return
}
defer response.Body.Close()
// 读取响应内容
body, err := ioutil.ReadAll(response.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
fmt.Println("Response from server:", string(body))
}
作为练习,尝试以下操作:
package main
import (
"encoding/json"
"fmt"
"net/http"
"sync"
)
// BlogPost 定义了博客文章的结构
type BlogPost struct {
Title string `json:"title"`
Content string `json:"content"`
Author string `json:"author"`
}
// blogPosts 存储了所有博客文章
var blogPosts = make([]BlogPost, 0)
var mutex sync.Mutex
func main() {
// 静态页面路由
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to the Go Blog!")
})
// 获取所有文章
http.HandleFunc("/posts", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
mutex.Lock()
postsJSON, _ := json.Marshal(blogPosts)
mutex.Unlock()
w.Header().Set("Content-Type", "application/json")
w.Write(postsJSON)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
})
// 添加新文章
http.HandleFunc("/posts/new", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
var newPost BlogPost
err := json.NewDecoder(r.Body).Decode(&newPost)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
mutex.Lock()
blogPosts = append(blogPosts, newPost)
mutex.Unlock()
w.WriteHeader(http.StatusCreated)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
})
// 监听并在8080端口启动服务器
fmt.Println("Server is listening on port 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("Error starting server:", err)
}
}
在这个例子中,我们创建了一个简单的HTTP服务器,它可以处理静态页面和JSON响应。我们还实现了一个简单的文章存储功能,可以通过HTTP
POST请求添加文章,并通过HTTP GET请求检索所有文章。这个练习为创建一个更复杂和功能丰富的博客系统奠定了基础。
通过完成第四章,读者应该能够理解Go语言在网络编程方面的基本概念,包括创建TCP和HTTP服务器、发送HTTP请求等。这些知识对于构建网络应用程序和服务是非常重要的。在下一章中,我们将学习如何将这些概念应用于我们的命令行博客系统,使其能够处理网络上的博客文章。
在本章中,我们将把网络编程的概念整合到我们的命令行博客系统中。我们的目标是使博客系统能够通过网络接收文章,并能够通过HTTP请求提供文章内容。
我们将设计一个简单的RESTful API,以便于通过HTTP方法管理博客文章,包括获取、创建和删除文章。
GET /posts
- 获取所有文章POST /posts
- 创建新文章GET /posts/{id}
- 获取特定ID的文章DELETE /posts/{id}
- 删除特定ID的文章我们将使用Go的net/http
包来实现上述API。
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"sync"
)
// BlogPost 定义了博客文章的结构
type BlogPost struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Author string `json:"author"`
}
// blogPosts 存储了所有博客文章
var blogPosts []BlogPost
var mutex sync.Mutex
var idCounter int
func main() {
http.HandleFunc("/posts", postsHandler)
http.HandleFunc("/posts/", postHandler)
// 监听并在8080端口启动服务器
fmt.Println("Server is listening on port 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("Error starting server:", err)
}
}
func postsHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
mutex.Lock()
postsJSON, _ := json.Marshal(blogPosts)
mutex.Unlock()
w.Header().Set("Content-Type", "application/json")
w.Write(postsJSON)
case http.MethodPost:
var newPost BlogPost
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
err = json.Unmarshal(bodyBytes, &newPost)
if err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
mutex.Lock()
idCounter++
newPost.ID = idCounter
blogPosts = append(blogPosts, newPost)
mutex.Unlock()
w.WriteHeader(http.StatusCreated)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func postHandler(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/posts/")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid post ID", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
found := false
for _, post := range blogPosts {
if post.ID == id {
postJSON, _ := json.Marshal(post)
w.Header().Set("Content-Type", "application/json")
w.Write(postJSON)
found = true
break
}
}
if !found {
http.NotFound(w, r)
}
case http.MethodDelete:
found := false
for i, post := range blogPosts {
if post.ID == id {
mutex.Lock()
blogPosts = append(blogPosts[:i], blogPosts[i+1:]...)
mutex.Unlock()
w.WriteHeader(http.StatusOK)
found = true
break
}
}
if !found {
http.NotFound(w, r)
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
我们将扩展命令行客户端,以便它可以与API服务器交互,获取和发布文章。
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run blogclient.go []" )
return
}
switch os.Args[1] {
case "list":
listPosts()
case "post":
if len(os.Args) != 5 {
fmt.Println("Usage: go run blogclient.go post " )
return
}
createPost(os.Args[2], os.Args[3], os.Args[4])
default:
fmt.Println("Unknown action")
}
}
func listPosts() {
response, err := http.Get("http://localhost:8080/posts")
if err != nil {
fmt.Println("Error fetching posts:", err)
return
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
var posts []BlogPost
err = json.Unmarshal(body, &posts)
if err != nil {
fmt.Println("Error decoding posts:", err)
return
}
for _, post := range posts {
fmt.Printf("ID: %d\nTitle: %s\nContent: %s\nAuthor: %s\n\n", post.ID, post.Title, post.Content, post.Author)
}
}
func createPost(title, content, author string) {
post := BlogPost{
Title: title,
Content: content,
Author: author,
}
postJSON, err := json.Marshal(post)
if err != nil {
fmt.Println("Error encoding post:", err)
return
}
response, err := http.Post("http://localhost:8080/posts", "application/json", bytes.NewBuffer(postJSON))
if err != nil {
fmt.Println("Error creating post:", err)
return
}
defer response.Body.Close()
if response.StatusCode == http.StatusCreated {
fmt.Println("Post created successfully")
} else {
fmt.Printf("Failed to create post, status code: %d\n", response.StatusCode)
}
}
作为练习,尝试以下操作:
// 添加到 main 函数的 switch-case 中
case "delete":
if len(os.Args) != 3 {
fmt.Println("Usage: go run blogclient.go delete " )
return
}
id := os.Args[2]
deletePost(id)
// ...
func deletePost(id string) {
client := &http.Client{}
req, err := http.NewRequest(http.MethodDelete, "http://localhost:8080/posts/"+id, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
fmt.Println("Post deleted successfully")
} else {
fmt.Printf("Failed to delete post, status code: %d\n", resp.StatusCode)
}
}
这需要在API服务器和客户端中添加对应的逻辑来处理PUT请求。
用户认证通常涉及到更复杂的逻辑和安全性考虑,例如使用JWT(JSON Web Tokens)或OAuth。这将超出本章的范围,但是作为一个练习,你可以探索如何在Go中实现这些认证机制。
通过完成第五章,读者应该能够理解如何将网络编程整合到命令行博客系统中,使得系统能够通过网络接收和发送数据。这些技能是构建现代网络应用程序的基础。在后续的章节中,我们可以进一步探讨如何扩展系统的功能,例如添加数据库支持、用户认证和更多的HTTP路由处理。
在本章中,我们将为我们的命令行博客系统添加持久化存储功能。这将允许我们的系统在服务重启后保留博客文章数据。我们将使用Go的database/sql
包来实现与SQLite数据库的交互。
我们将创建一个简单的数据库模型,用于存储博客文章。
go mod init myblog
go mod tidy
项目结构
- project
- db
- db.go
- model
- models.go
- api-server.go
- cmd-client.go
CREATE TABLE IF NOT EXISTS posts
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT,
title
TEXT
NOT
NULL,
content
TEXT
NOT
NULL,
author
TEXT
NOT
NULL
);
我们将实现一个简单的数据库操作层,用于执行CRUD(创建、读取、更新和删除)操作。
package db
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
"log"
"myblog/model"
)
var db *sql.DB
func InitDB(filePath string) {
var err error
db, err = sql.Open("sqlite3", filePath)
if err != nil {
log.Fatal(err)
}
createTableSQL := `CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
author TEXT NOT NULL
);`
_, err = db.Exec(createTableSQL)
if err != nil {
log.Fatal(err)
}
}
func GetPosts() ([]*model.BlogPost, error) {
rows, err := db.Query("SELECT id, title, content, author FROM posts")
if err != nil {
return nil, err
}
defer rows.Close()
var posts []*model.BlogPost
for rows.Next() {
var post model.BlogPost
if err := rows.Scan(&post.ID, &post.Title, &post.Content, &post.Author); err != nil {
return nil, err
}
posts = append(posts, &post)
}
return posts, nil
}
func CreatePost(post model.BlogPost) (int64, error) {
result, err := db.Exec("INSERT INTO posts (title, content, author) VALUES (?, ?, ?)", post.Title, post.Content, post.Author)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
func GetPostByID(id int) (*model.BlogPost, error) {
row := db.QueryRow("SELECT id, title, content, author FROM posts WHERE id = ?", id)
var post model.BlogPost
if err := row.Scan(&post.ID, &post.Title, &post.Content, &post.Author); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &post, nil
}
func DeletePostByID(id int) error {
_, err := db.Exec("DELETE FROM posts WHERE id = ?", id)
return err
}
我们需要修改API服务器的代码,以使用数据库操作层来处理数据。
// ... 保留之前的代码 ...
func main() {
initDB("blog.db")
http.HandleFunc("/posts", postsHandler)
http.HandleFunc("/posts/", postHandler)
// 监听并在8080端口启动服务器
fmt.Println("Server is listening on port 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("Error starting server:", err)
}
}
func postsHandler(w http.ResponseWriter, r *http.Request) {
// ... 保留之前的代码 ...
case http.MethodPost:
var newPost BlogPost
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
err = json.Unmarshal(bodyBytes, &newPost)
if err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
id, err := createPost(newPost)
if err != nil {
http.Error(w, "Error saving post", http.StatusInternalServerError)
return
}
newPost.ID = int(id)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(newPost)
w.WriteHeader(http.StatusCreated)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
// ... 修改其他处理函数以使用数据库操作 ...
作为练习,尝试以下操作:
// 添加到数据库操作代码中
func UpdatePostByID(id int, post model.BlogPost) error {
_, err := db.Exec("UPDATE posts SET title = ?, content = ?, author = ? WHERE id = ?", post.Title, post.Content, post.Author, id)
return err
}
// 添加到API服务器中的 postHandler
case http.MethodPut:
var updatedPost BlogPost
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
err = json.Unmarshal(bodyBytes, &updatedPost)
if err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
err = updatePostByID(id, updatedPost)
if err != nil {
http.Error(w, "Error updating post", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
用户认证通常涉及到更复杂的逻辑和安全性考虑,例如使用JWT(JSON Web Tokens)或OAuth。这将超出本章的范围,但是作为一个练习,你可以探索如何在Go中实现这些认证机制。
通过完成第六章,读者应该能够理解如何为Go语言编写的博客系统添加持久化存储功能。我们介绍了如何使用SQLite数据库来存储数据,并展示了如何将数据库操作集成到我们的API服务器中。这些知识是构建能够长期存储数据的网络应用程序的基础。在后续的章节中,我们可以进一步探讨如何扩展系统的功能,例如添加更复杂的数据库操作、用户认证和安全性措施。
在本章中,我们将为我们的命令行博客系统添加用户认证和授权功能。这将确保只有经过验证的用户才能创建、更新或删除文章。我们将使用JSON
Web Tokens(JWT)来实现这一功能。
首先,我们需要设计一个用户模型来存储用户信息。
CREATE TABLE IF NOT EXISTS users
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT,
username
TEXT
NOT
NULL
UNIQUE,
password_hash
TEXT
NOT
NULL
);
我们将允许用户注册和登录,以便我们可以发行JWT给认证用户。
package main
import (
"database/sql"
"fmt"
"log"
"golang.org/x/crypto/bcrypt"
"github.com/dgrijalva/jwt-go"
)
var jwtKey = []byte("my_secret_key")
type Claims struct {
Username string `json:"username"`
jwt.StandardClaims
}
// Register a new user
func registerUser(username, password string) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8)
if err != nil {
return err
}
_, err = db.Exec("INSERT INTO users (username, password_hash) VALUES (?, ?)", username, hashedPassword)
return err
}
// Authenticate a user and return a JWT
func authenticateUser(username, password string) (string, error) {
// Verify the username and password
var passwordHash string
row := db.QueryRow("SELECT password_hash FROM users WHERE username = ?", username)
err := row.Scan(&passwordHash)
if err == sql.ErrNoRows {
return "", fmt.Errorf("user not found")
} else if err != nil {
return "", err
}
err = bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password))
if err != nil {
return "", fmt.Errorf("invalid password")
}
// Create a new token object, specifying signing method and the claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &Claims{
Username: username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: 15000, // Token expires in 15 seconds for demonstration purposes
},
})
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}
现在我们需要修改API服务器的代码,以验证JWT并根据用户权限处理请求。
// ... 保留之前的代码 ...
func main() {
// ... 保留之前的代码 ...
http.HandleFunc("/register", registerHandler)
http.HandleFunc("/login", loginHandler)
// ... 保留之前的代码 ...
}
func registerHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
err = registerUser(credentials.Username, credentials.Password)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
tokenString, err := authenticateUser(credentials.Username, credentials.Password)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Token", tokenString)
w.WriteHeader(http.StatusOK)
}
// Middleware to protect private routes
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func (token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
if !token.Valid {
w.WriteHeader(http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// ... 使用 authMiddleware 包装需要保护的路由 ...
作为练习,尝试以下操作:
用户注销功能通常涉及到使当前的JWT失效。这可以通过在服务器端维护一个失效的token列表来实现,或者通过设置JWT的exp
(过期时间)字段为当前时间来使其立即失效。
密码重置功能通常涉及到发送一次性链接到用户注册的电子邮件地址,用户可以通过该链接来重置他们的密码。
更复杂的权限系统可能需要在用户模型中添加角色字段,并在认证时检查用户角色以确定他们是否有权执行特定操作。
通过完成第七章,读者应该能够理解如何在Go语言编写的博客系统中添加用户认证和授权功能。我们介绍了如何使用JWT来验证用户,并保护API路由以确保只有认证用户才能执行某些操作。这些知识是构建安全网络应用程序的基础。在后续的章节中,我们可以进一步探讨如何扩展系统的安全性,例如通过HTTPS提供服务、实现密码策略和添加更多的安全性措施。