第一部分:构建基础命令行博客系统
章节 1:Go语言快速入门
1.1 Go语言简介
Go语言,也称作Golang,是由Google开发的一种静态强类型、编译型语言,具有垃圾回收功能。它在2009年公开发布,由Robert Griesemer、Rob Pike和Ken Thompson设计。Go语言的设计目标是为了解决大型软件系统的构建问题,特别是在Google内部,这些系统需要高效的编译、高效的执行以及高效的代码维护。
Go的主要特点包括:
- 简洁、快速和安全
- 支持并发,通过goroutines和channels轻松实现
- 丰富的标准库,尤其在网络服务和并发处理方面
- 简单的依赖管理
- 跨平台,支持多种操作系统
Go语言适用于各种类型的项目,从小型个人项目到大型分布式系统。在本书中,我们将使用Go语言构建一个博客系统,这将帮助我们理解Go语言在实际应用中的强大功能。
1.2 安装和设置Go开发环境
让我们开始安装Go语言。请访问Go语言官方网站(https://golang.org/dl/)下载适合您操作系统的安装包。下载完成后,请按照官方指南完成安装。
安装Go后,您可以打开终端或命令提示符并运行以下命令来验证安装:
sh
go version
这应该会显示安装的Go版本。例如:
sh
go version go1.15.6 linux/amd64
接下来,设置您的工作空间。Go语言的工作空间是存放Go代码的地方。它有一个特定的目录结构:
src
目录包含Go的源文件,pkg
目录包含包对象,bin
目录包含可执行文件。
您可以通过设置环境变量GOPATH
来指定您的工作空间目录。例如,在Unix系统上:
sh
export GOPATH=$HOME/go
在Windows系统上:
sh
set GOPATH=c:\go
1.3 Hello World程序
编写Hello World程序是学习新编程语言的传统。在Go中,这个程序看起来是这样的:
go
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
将上面的代码保存为hello.go
。然后在命令行中运行以下命令来编译并运行程序:
sh
go run hello.go
如果一切顺利,您将看到终端打印出"Hello, World!"。
1.4 Go程序基本结构
Go程序由包(packages)组成。每个Go文件都属于一个包,且文件的第一行声明了它所属的包。main
包是特殊的,它告诉Go编译器这个程序是可执行的,而不是一个库。
在main
包中,main
函数也是特殊的------它是程序执行的入口点。在上面的Hello World程序中,我们导入了fmt
包,这是一个包含I/O函数的标准库包。我们使用fmt.Println
来输出字符串到标准输出。
1.5 练习:编写第一个Go程序
现在是时候动手写代码了。作为练习,请尝试以下操作:
- 编写一个Go程序,打印出你最喜欢的引语。
- 修改程序,接收用户输入,并打印出一个个性化的问候语。
这是一个接收用户输入的示例程序:
go
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语言的核心概念和功能。
章节 2:Go语言基础
2.1 变量和数据类型
在Go语言中,变量是存储程序执行过程中数据的容器。Go是静态类型语言,这意味着变量是有明确定义的类型,类型在编译时就已确定,并且类型在整个程序运行期间不会改变。
声明变量
go
var message string
message = "Hello, Go!"
// 或者一步到位
var greeting = "Hello, Go!"
// 短变量声明,最常用
name := "World"
基本数据类型
Go语言中的基本数据类型包括:
- 整型(int、uint、int8、int16、int32、int64等)
- 浮点型(float32、float64)
- 布尔型(bool)
- 字符串(string)
2.2 控制结构
控制结构在Go语言中用于控制程序的执行流程。Go提供了多种控制结构,例如if语句、for循环和switch语句。
If语句
go
if number := 10; number%2 == 0 {
fmt.Println(number, "is even")
} else {
fmt.Println(number, "is odd")
}
For循环
go
// 标准的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)
Switch语句
go
dayOfWeek := 3
switch dayOfWeek {
case 1:
fmt.Println("Monday")
case 2:
fmt.Println("Tuesday")
case 3:
fmt.Println("Wednesday")
// ...
default:
fmt.Println("Invalid day")
}
2.3 函数定义和返回值
函数是执行特定任务的代码块。在Go中,您可以定义带有参数和返回值的函数。
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)
2.4 错误处理基础
在Go中,错误处理是通过返回一个错误类型的值来完成的。如果一个函数可能产生错误,它通常是函数返回值列表中的最后一个。
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)
2.5 练习:创建基本的输入输出函数
作为练习,尝试以下操作:
- 创建一个函数,接受两个字符串参数并返回它们的拼接结果。
- 编写一个函数,接受一个整数数组并返回它们的和。
- 实现一个函数,接受一个整数并返回它的阶乘。
字符串拼接函数
go
func concatenate(str1, str2 string) string {
return str1 + str2
}
fmt.Println(concatenate("Hello, ", "Go!"))
整数数组求和函数
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}))
阶乘函数
go
func factorial(n int) int {
if n == 0 {
return 1
}
return n * factorial(n-1)
}
fmt.Println(factorial(5))
通过完成第二章,读者应该能够理解Go语言的变量、数据类型、控制结构、函数定义以及错误处理的基础知识。这些是构建更复杂程序的基石。在下一章中,我们将探索Go的标准库,它提供了大量方便的工具和函数,可以帮助我们更快地开发程序。
章节 3:探索Go的标准库
Go的标准库是一组广泛的包,提供了从输入/输出处理到网络编程的功能。在这一章节中,我们将探索一些对于构建命令行博客系统非常有用的标准库包。
3.1 使用fmt
包
fmt
包实现了格式化的I/O函数,类似于C语言的printf和scanf。我们已经在前面的Hello World程序中使用了fmt.Println
来输出文本。
格式化输出
go
name := "Go Programmer"
age := 30
fmt.Printf("My name is %s and I am %d years old.\n", name, age)
从标准输入读取
go
var input string
fmt.Print("Enter your input: ")
fmt.Scanln(&input)
fmt.Println("You entered:", input)
3.2 文件操作:io/ioutil
和os
包
文件操作是大多数程序中的常见任务。在Go中,io/ioutil
和os
包提供了这方面的功能。
读取文件
go
content, err := ioutil.ReadFile("blogpost.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println("File content:", string(content))
写入文件
go
message := []byte("Hello, Go Blog!")
err := ioutil.WriteFile("blogpost.txt", message, 0644)
if err != nil {
log.Fatal(err)
}
检查文件是否存在
go
if _, err := os.Stat("blogpost.txt"); os.IsNotExist(err) {
fmt.Println("The file does not exist.")
} else {
fmt.Println("The file exists.")
}
3.3 日期和时间:time
包
处理日期和时间是编程中的一个常见需求。Go的time
包提供了这方面的功能。
获取当前时间
go
now := time.Now()
fmt.Println("Current time:", now)
格式化日期和时间
go
fmt.Println("Formatted time:", now.Format("2006-01-02 15:04:05"))
计算时间差
go
past := time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC)
duration := now.Sub(past)
fmt.Println("Duration since past:", duration)
3.4 处理JSON:encoding/json
包
JSON是一种轻量级的数据交换格式,经常被用于网络通信。在Go中,encoding/json
包提供了JSON数据的编码和解码功能。
JSON编码
go
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))
JSON解码
go
var post BlogPost
err := json.Unmarshal(jsonBytes, &post)
if err != nil {
log.Fatal(err)
}
fmt.Println("Blog post:", post)
3.5 练习:创建并操作自己的博客文章
作为练习,尝试以下操作:
- 创建一个结构体表示博客文章,包括标题、内容、作者和发布日期。
- 编写一个函数将博客文章结构体编码为JSON。
- 编写另一个函数将JSON解码回博客文章结构体。
- 将博客文章保存到文件,并从文件中读取。
博客文章结构体
go
type BlogPost struct {
Title string `json:"title"`
Content string `json:"content"`
Author string `json:"author"`
Date time.Time `json:"date"`
}
编码为JSON
go
func encodeToJSON(post BlogPost) ([]byte, error) {
return json.Marshal(post)
}
解码JSON
go
func decodeFromJSON(jsonBytes []byte) (BlogPost, error) {
var post BlogPost
err := json.Unmarshal(jsonBytes, &post)
return post, err
}
保存和读取博客文章
go
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的网络编程功能来让我们的博客系统可以通过网络进行数据交换。
章节 4:Go的网络编程
在这一章节中,我们将介绍Go语言在网络编程方面的能力。Go的net
包提供了丰富的网络编程功能,包括TCP/UDP协议、HTTP客户端和服务端的实现等。
4.1 创建TCP服务器
TCP(传输控制协议)是一种可靠的、面向连接的协议。下面的例子演示了如何创建一个简单的TCP服务器,它监听本地端口,并回显接收到的消息。
TCP Echo服务器
go
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))
}
}
4.2 创建HTTP服务器
Go的net/http
包让创建HTTP服务器变得非常简单。下面的代码展示了如何创建一个基本的HTTP服务器,它可以响应GET请求。
HTTP服务器
go
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
}
}
4.3 创建HTTP客户端
Go的net/http
包不仅可以创建服务器,还可以作为客户端发送请求。以下示例展示了如何发送GET请求并读取响应。
HTTP客户端
go
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))
}
4.4 练习:构建一个简单的博客服务器
作为练习,尝试以下操作:
- 创建一个HTTP服务器,它可以处理不同的路由和HTTP方法。
- 服务器应该能够响应至少两种内容:静态页面和JSON响应。
- 实现简单的文章存储功能,可以通过HTTP请求添加和检索文章。
HTTP服务器处理不同路由
go
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请求等。这些知识对于构建网络应用程序和服务是非常重要的。在下一章中,我们将学习如何将这些概念应用于我们的命令行博客系统,使其能够处理网络上的博客文章。
章节 5:整合网络功能到命令行博客系统
在本章中,我们将把网络编程的概念整合到我们的命令行博客系统中。我们的目标是使博客系统能够通过网络接收文章,并能够通过HTTP请求提供文章内容。
5.1 设计RESTful API
我们将设计一个简单的RESTful API,以便于通过HTTP方法管理博客文章,包括获取、创建和删除文章。
GET /posts
- 获取所有文章POST /posts
- 创建新文章GET /posts/{id}
- 获取特定ID的文章DELETE /posts/{id}
- 删除特定ID的文章
5.2 实现API服务器
我们将使用Go的net/http
包来实现上述API。
服务器代码
go
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)
}
}
5.3 命令行客户端功能
我们将扩展命令行客户端,以便它可以与API服务器交互,获取和发布文章。
客户端代码
go
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 <action> [<params>]")
return
}
switch os.Args[1] {
case "list":
listPosts()
case "post":
if len(os.Args) != 5 {
fmt.Println("Usage: go run blogclient.go post <title> <content> <author>")
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)
}
}
5.4 练习:扩展博客功能
作为练习,尝试以下操作:
- 添加命令行客户端的删除功能,以便它可以通过HTTP请求删除文章。
- 实现文章更新功能,允许通过PUT请求更新现有文章的内容。
- 添加用户认证,确保只有认证用户才能创建、更新和删除文章。
添加删除功能
go
// 添加到 main 函数的 switch-case 中
case "delete":
if len(os.Args) != 3 {
fmt.Println("Usage: go run blogclient.go delete <id>")
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路由处理。
章节 6:为博客系统添加持久化存储
在本章中,我们将为我们的命令行博客系统添加持久化存储功能。这将允许我们的系统在服务重启后保留博客文章数据。我们将使用Go的database/sql
包来实现与SQLite数据库的交互。
6.1 设计数据库模型
我们将创建一个简单的数据库模型,用于存储博客文章。
使用go mod创建项目
shell
go mod init myblog
go mod tidy
项目结构
text
- project
- db
- db.go
- model
- models.go
- api-server.go
- cmd-client.go
数据库模型
sql
CREATE TABLE IF NOT EXISTS posts
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT,
title
TEXT
NOT
NULL,
content
TEXT
NOT
NULL,
author
TEXT
NOT
NULL
);
6.2 实现数据库操作
我们将实现一个简单的数据库操作层,用于执行CRUD(创建、读取、更新和删除)操作。
数据库操作代码
go
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
}
6.3 集成数据库操作到API服务器
我们需要修改API服务器的代码,以使用数据库操作层来处理数据。
修改后的API服务器代码
go
// ... 保留之前的代码 ...
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)
}
}
// ... 修改其他处理函数以使用数据库操作 ...
6.4 练习:添加更新和认证功能
作为练习,尝试以下操作:
- 实现更新文章的功能,允许通过PUT请求更新现有文章的内容。
- 添加基本的用户认证,确保只有认证用户才能创建、更新和删除文章。
实现更新文章功能
go
// 添加到数据库操作代码中
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服务器中。这些知识是构建能够长期存储数据的网络应用程序的基础。在后续的章节中,我们可以进一步探讨如何扩展系统的功能,例如添加更复杂的数据库操作、用户认证和安全性措施。
章节 7:增加用户认证和授权
在本章中,我们将为我们的命令行博客系统添加用户认证和授权功能。这将确保只有经过验证的用户才能创建、更新或删除文章。我们将使用JSON
Web Tokens(JWT)来实现这一功能。
7.1 设计用户模型
首先,我们需要设计一个用户模型来存储用户信息。
用户模型
sql
CREATE TABLE IF NOT EXISTS users
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT,
username
TEXT
NOT
NULL
UNIQUE,
password_hash
TEXT
NOT
NULL
);
7.2 实现用户注册和登录
我们将允许用户注册和登录,以便我们可以发行JWT给认证用户。
用户注册和登录代码
go
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
}
7.3 集成认证到API服务器
现在我们需要修改API服务器的代码,以验证JWT并根据用户权限处理请求。
修改后的API服务器代码
go
// ... 保留之前的代码 ...
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 包装需要保护的路由 ...
7.4 练习:扩展认证功能
作为练习,尝试以下操作:
- 实现用户注销功能,使得JWT在用户注销时失效。
- 添加密码重置功能,允许用户通过某种机制重置他们的密码。
- 实现更复杂的权限系统,允许不同的用户有不同的操作权限。
实现用户注销功能
用户注销功能通常涉及到使当前的JWT失效。这可以通过在服务器端维护一个失效的token列表来实现,或者通过设置JWT的exp
(过期时间)字段为当前时间来使其立即失效。
添加密码重置功能
密码重置功能通常涉及到发送一次性链接到用户注册的电子邮件地址,用户可以通过该链接来重置他们的密码。
实现更复杂的权限系统
更复杂的权限系统可能需要在用户模型中添加角色字段,并在认证时检查用户角色以确定他们是否有权执行特定操作。
通过完成第七章,读者应该能够理解如何在Go语言编写的博客系统中添加用户认证和授权功能。我们介绍了如何使用JWT来验证用户,并保护API路由以确保只有认证用户才能执行某些操作。这些知识是构建安全网络应用程序的基础。在后续的章节中,我们可以进一步探讨如何扩展系统的安全性,例如通过HTTPS提供服务、实现密码策略和添加更多的安全性措施。