前言:大家好啊,我是绣虎,一名来自双非的大三学生,这是我发的第一篇博客。由于上大学之后就一直沉迷于电脑游戏,生活和学习相去甚远,可谓是宁可参加田径队、篮球队、辩论赛也是不愿意静下心来好好学习,导致的结果就是又是挂科又是重修,所幸绩点还算稳在能拿学位证的程度。
而在某一天的清晨,站在校园中,感觉自己和大家欣欣向荣的氛围格格不入时,突然一阵心潮涌动,仿佛感受到了什么,瞧着自己日益苍老的父母,是否我也该为未来考虑一下了,俗话说浪子回头金不换,想到什么先做就完事了,于是就有了这个计划:我要通过学习Go语言,去拿到大厂的offer。 也许这样的梦想过于远大,但是我只知道,我现在想做成这样的事情,因此我就此为起点,开始我的行动。
我在这一周学了很多基础的技能,比如git、markdown、apifox这些工具。
在一开始,我将我的计划每天都存进github中,但是经常遇到github关闭的情况,虽然上帝为我关上了这扇门,但感谢他也为我指引了一条新的路,我了解到了gitee。于是在之后的第二周开始我会同步上传gitee,而github我们就让它随缘吧。
最后,由于个人水平和风格偏向娱乐,我会在之后po上这两个网站的地址,以供诸位看客们、同僚前辈们闲暇之余阅读取乐。 : )
第一周完成的任务
Go环境搭建成功
我用的GoRoot是1.26.0,
说实话,一开始看的教学是让我去下载1.21.x的,前面啥困难都没有打击到我。但是教学进行到这一步着实给我一个很大的冲击,让我小小的动摇了一下。心想学这技术咱是不是学晚了些?但转念一想到自己的情况,嘿嘿,我这是傻子笑瘸子呢,自己傻还笑别人残疾。人家技术增长是好事,于是定下心来继续学
配置环境的过程很顺利,因为我有c和java的基础,gopath这些基本上是只花了十几分钟就弄好了。并成功运行了第一个go程序,熟悉的“hello,world!”
第一个HTTP服务运行
这是一个我到处翻找发现的简易Go HTTP代码,其中包含有一些很基础的东西,数据结构是:响应结构体、用户结构体和一个用map封装的内存存储。
里面通过 /api/user/、 /health、 /等逐层传输,刚开始并不理解这些东西,后来用apifox调试的时候学习知道这是我的这个简易代码的路由,如果我想通过网页找到相关信息就必须输入这些路由(网址:http://localhost:8080/api/user")这样的格式。
随后,为HTTP中添加了用户管理系统的CRUD功能,这也是本周的主要工作之一,代码我会在后面端上来给大家看看这一份我的小小尝试品。
学到的知识
Go基础语法
-
Go语言是一门灵活的语言,在它的身上我看到了很多java的影子,这也是我决定以Go为主以java为辅的原因之一。
-
变量的定义 变量分为:局部变量 和 全局变量
局部变量 一经定义就必须要使用,不然就报错,局部变量有四种定义方法
全局变量 定义在方法体外,类之内,整个类的方法都能用这个变量
package main
// 调用一个输出包
// import "fmt"
// 当要调用的包比较多,我们可以这样
import (
"fmt"
// 当然不调用也可以,但这里我们先调用,同时一定要锁定根目录的文件,以下是示范,请你使用的时候换成自己的vertion包的路径地址
"/xxx/xxx/vertion"
)
// 现在定义全局变量
// 即使没人调用也不会报红
var a = 12
// 全局变量中定义多个变量
var (
s1 string = "在外面"
s2 string = "不报错"
)
func hello(){
// 在这个方法中无法调用main中变量
fmt.Println("你好")
// 但在这个方法中可以用a,因为a是全局变量
fmt.Println(a)
}
func main(){
// 这样就能调用另一个vertion包中的内容了
fmt.Println(vertion.Vertion)
// 但不能调用小写的vertion,这样会报错
// fmt.Println(vertion)
// 现在定义局部变量
// 先声明
var name string
// 再赋值
name = "ad"
// 定义了不使用name就会标红
fmt.Println(name)
// 直接声明赋值
var name1 string = "Lee"
fmt.Println(name1)
// 不用定义类型,也能自动识别
var name2 = "lili"
fmt.Println(name2)
// 还有快速定义的方法,这也叫短声明
name3 := "niuniu"
fmt.Println(name3)
// 调用方法
hello()
// 定义多个变量
var a1,a2= 2,3
fmt.Println(a1,a2)
}
让我们再定义一个包vertion,来试试什么样的方法可以被跨包调用
package vertion
// 这是一个常量const,定义以后就不会再改变
// 请注意命名规范,只有首字母大写的常量或变量才能被跨包调用
const Vertion string = "1.0.1"
const vertion string = "2.0.1"
-
Go中的输入和输出
Go中的输入输出也有一定的讲究,并且充满趣味性
package main
import "fmt"
func main() {
// 首先是输出
// 三种print,print非格式化输出不换行不空格;println自动换行;printf是格式化输出字符串
// 不声明类型不能输出整数等类型,默认是字符串
fmt.Print(1)
fmt.Printf("1\n")
fmt.Printf("%s是个超绝大美女", "灰灰大王")
fmt.Println("我没有换行,我在美女后面")
fmt.Print("我换行了,我在美女下面")
// 格式化输出的一些常用符号
// 可以作为任何值的占位符输出
fmt.Printf("%v\n", "你好")
// 打印类型T
fmt.Printf("%v %T\n", "niu", "er")
// 整数
fmt.Printf("%d\n", 3)
// 小数
fmt.Printf("%.2f\n", 1.25)
// 字符串
fmt.Printf("%s\n", "length")
// 用go的语法格式输出,很适合打印空字符串
fmt.Printf("%#v\n", "")
// 还有一个常用的将格式化后的内容赋值给一个变量
name := fmt.Sprintf("%v", "你好")
fmt.Println(name)
// 输入
fmt.Print("请输入你的名字:")
var name1 string
// &指针,指向这个定义的变量,我个人感觉变量存在方法里,应该是在栈中存储
fmt.Scan(&name1)
fmt.Println(name1)
fmt.Print("请输入你的年龄:")
var age int
// 这里额外展示一次表示不限数据类型
// fmt.Scan(&age)
// 可以用另一个短定义的方式接住
n, err := fmt.Scan(&age)
fmt.Println(n, err, age)
}
- 基本数据类型
int类型
- 默认的数字定义类型是int类型
- 带个u就是无符号,只能存正整数
- 后面的数字就是2进制的位数(uint8:8位无符号二进制)
- uint8还有个别名是byte,一个字节=8个bit位
- int类型的大小取决于所使用的平台,所以为了易用性,还是声明位数较好
- 示例:
- uint8
0 0 0 0 0 0 0 0 = 0
1 1 1 1 1 1 1 1 = 255 = 2^8-1- int8
0 0 0 0 0 0 0 0 = 0
0 1 1 1 1 1 1 1 = 127
1 0 0 0 0 0 0 1 = -1 (原码) 补码:(取反=反码+1)
1 0 0 0 0 0 0 0 = -128
小数类型
一个float32和float64,可以计算小数位
字符型
- 掌握byte(单字节字符)和rune(多字节字符)即可,前者适用非汉语体系字符,后者适用汉语体系字符。
- 在Go中,字符的本质是一个整数,直接输出时,是该字符对应的UTF-8编码的码值。
- 可以直接给某个变量赋一个数字,然后按照格式化输出时%c,会输出该数字对应的unicode字符。
- 字符类型是可以进行运算的,因为它们相当于一个整数,都有各自对应的unicode码。
字符串类型
和字符类型不一样的是,字符类型赋值用的是单引号,字符串用双引号
转义字符 记住常用的就好
\t:制表符(空格)\n:回车\"内容\":添加双引号\r:回到行首(前面输入的都清空)\\内容\\:反斜杠(让内容两旁出现斜杠)
布尔类型
布尔类型只有真(true)假(false)两个值
- 默认值为false
- Go语言中不允许将整型类型转为布尔类型
- 布尔类型无法参与数值运算,也无法与其他类型进行转换
零值问题
如果一个基本数据类型只声明不赋值,那么这个变量的值就是对应类型的零值
-
int:0
-
bool:false
-
string:""
-
float32:0
-
pointer:nil
数据结构:结构体、切片、映射
结构体
结构体定义需要用type和struct语句。struct语句定义一个新的数据类型,结构体中有一个或多个成员。type语句设定了结构体的名称。
切片
Go语言切片是对数组的抽象,Go数组的长度不可改变,在某些场景这样的集合就不太适用,这里提供一个新的更为灵活的内置类型切片(“动态数组”),可以不断追加元素,在追加时会根据是否越界使切片的容量增大
映射
较为复杂,所以略加一些详细的内容po上来:
- 关键特性:引用类型、无序、key唯一、必须初始化
- 核心操作:赋值、取值、遍历、删除、判存在
-
Map 是一种无序的键值对的集合。
-
Map 最重要 的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
-
Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,遍历 Map 时返回的键值对的顺序是不确定的。
-
在获取 Map 的值时,如果键不存在,返回该类型的零值,例如 int 类型的零值是 0,string 类型的零值是 "" 。
-
Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 Map 的修改会影响到所有引用它的变量。
-
必背:
-
map是引用类型 , 必须用 make 初始化才能赋值
-
Key 必须可比较 (不能是切片/函数/集合), value任意类型
-
取值一定要用 value, ok := map[key] 判断 key 是否存在
-
遍历无序, 不要依赖遍历顺序
-
无内置清空函数,直接make新map即可
-
HTTP处理函数
在Go语言中,HTTP处理函数是用于处理HTTP请求的核心组件:
基本处理函数
// HandlerFunc 类型
type HandlerFunc func(ResponseWriter, *Request)
// Handler 接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
创建处理函数
- 方式一:普通函数
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
// 注册
http.HandleFunc("/hello", helloHandler)
- 方式二:实现Handler接口
type MyHandler struct{}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from MyHandler")
}
// 注册
handler := &MyHandler{}
http.Handle("/custom", handler)
常用处理模式
- 带参数的路径
func userHandler(w http.ResponseWriter, r *http.Request) {
// 获取路径参数
vars := mux.Vars(r) // 使用 gorilla/mux
id := vars["id"]
fmt.Fprintf(w, "User ID: %s", id)
}
- 处理不同HTTP方法
func apiHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
// 处理 GET 请求
case "POST":
// 处理 POST 请求
case "PUT":
// 处理 PUT 请求
case "DELETE":
// 处理 DELETE 请求
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
中间件模式
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// 使用中间件
handler := http.HandlerFunc(apiHandler)
http.Handle("/api", loggingMiddleware(authMiddleware(handler)))
JSON响应处理
初始化响应结构体,然后调用处理函数来访问
type Response struct {
Status string `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func jsonHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := Response{
Status: "success",
Message: "Request processed",
Data: map[string]string{"key": "value"},
}
json.NewEncoder(w).Encode(response)
}
错误处理
func errorHandler(w http.ResponseWriter, r *http.Request) {
// 返回错误状态码
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
// 或者自定义错误响应
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, `{"error": "Invalid request"}`)
}
JSON编解码
Go 标准库 encoding/json 负责 JSON 与 Go 数据类型的互转(编 / 解码)
-
编码(Marshal/Encode)
-
定义:将 Go 结构体(struct)、map 等类型转换为 JSON 字符串(字节流 []byte)
-
常用方式:
- json.Marshal(v):直接将数据序列化为 []byte
- json.NewEncoder(w).Encode(v):将数据直接编码并写入 io.Writer(如 HTTP 响应体 http.ResponseWriter)
-
规则:
- 结构体字段必须首字母大写才能被导出(被 json 包访问)
- 标签 json:"name" 指定 JSON 中的字段名,如 Status string \json:"status"``
- omitempty:如果字段值为空(0 值、nil、空字符串),序列化时会忽略该字段
-
-
解码(Unmarshal/Decode)
-
定义:将 JSON 字符串反序列化为 Go 的结构体或 map
-
常用方式:
-
json.Unmarshal(data, &v):从字节流解析到变量
-
json.NewDecoder(r).Decode(&v):从 io.Reader(如 HTTP 请求体 r.Body)中读取并解析
-
-
空接口 interface{}:用于接收不确定类型的数据,反序列化时会映射为map[string]interface{}
-
并发安全(sync.RWMutex)
sync.RWMutex:读写锁,适用于读多写少的场景,因为允许读并发
Lock()(读锁,独占)、UnLock()、RLock()(写锁,共享)、RUnlock()
而sync.Mutex,属于通用场景,读写都不允许并发
遇到的困难
计划过于理想
一开始的安排有点没轻没重,给自己弄得有些焦头烂额了,时间安排上都略显不合理,在新的一周里,会做一些适当的调整
时间管理不足
因为一些坏习惯,导致任务进度拖沓。包括朋友之间打游戏,还有健身之类的安排,互相交错的时间安排留给学习的就不多了。
完美主义拖延
很多看不懂的东西,有时候到处去查找知识点,就要花个几小时,同时为了搞明白知识,误入一些现在来说太遥远的深水区,学习效率低就算了,还大大挤兑了一些简单知识的理解。
代码展示
含有用户管理CRUD功能的Go HTTP的代码
package main
//包与结构体初始化
import (
"encoding/json"
"fmt"
"log"
"time"
//"log"
"net/http" //基于TCP/IP实现的HTTP协议上层封装
"strconv"
"sync"
)
// 通用响应结构体设计:Response
type Response struct {
Status string `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Time string `json:"timestamp"`
}
// 用户结构体
type User struct {
ID int `json:"id"`
Username string `json:"name"`
Email string `json:"email"`
}
// 内存存储
var (
users = make(map[int]User)
nextID = 1
userMux sync.RWMutex
)
func main() {
// 路由注册
// 1. 用户信息接口
//handlefunc将普通函数func()转换为handler接口
//http.ResponseWriter:响应写入器,用于向客户端返回HTTP响应(状态码、响应头、响应体
//*http.Request:请求结构体,封装了客户端的 HTTP 请求信息(请求方法、路径、参数、请求体等
http.HandleFunc("/api/user/", func(w http.ResponseWriter, r *http.Request) {
//HTTP响应头,用于指定MIME类型
//以下的Content-Type没有设置(text/html),这也是为什么网页显示乱码的原因
w.Header().Set("Content-Type", "application/json")
// 简化:总是返回第一个用户
response := Response{
Status: "success",
Message: "获取用户成功",
Data: users[1],
Time: time.Now().Format(time.RFC3339),
}
json.NewEncoder(w).Encode(response)
})
// 2. 健康检查接口
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
"time": time.Now().Format("2006-01-02 15:04:05"),
})
})
// 3. 根路径
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
html := `
<!DOCTYPE html>
<html>
<head><title>Go学习服务</title></head>
<body>
<h1>Go HTTP服务运行中</h1>
<p>可用接口:</p>
<ul>
前缀匹配
<li><a href="/api/user/">GET /api/user/</a> - 获取用户信息</li>
<li><a href="/health">GET /health</a> - 健康检查</li>
</ul>
</body>
</html>`
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(html))
})
// 添加用户管理API
http.HandleFunc("/api/users", handleUsers)
http.HandleFunc("/api/users/", handleUserByID)
// 启动服务器
port := ":8080"
fmt.Printf("服务器启动:http://localhost%s\n", port)
fmt.Println("按 Ctrl+C 停止")
// - 在8080端口启动*TCP监听器*(net.Listen("tcp", port));
// - 为每个客户端的TCP链接*创建一个独立的GoRoutine*处理请求,无需手动管理并发
log.Fatal(http.ListenAndServe(port, nil))
}
// 处理/users
func handleUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
// 获取所有用户
userMux.RLock()
userList := make([]User, 0, len(users))
for _, user := range users {
userList = append(userList, user)
}
userMux.RUnlock()
json.NewEncoder(w).Encode(userList)
case http.MethodPost: //"post"
// 创建用户
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) //400
return
}
userMux.Lock()
user.ID = nextID
users[nextID] = user
nextID++
userMux.Unlock()
w.WriteHeader(http.StatusCreated) //201
json.NewEncoder(w).Encode(user)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) //405
}
}
// 处理/users/{id}
func handleUserByID(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// 解析ID
idStr := r.URL.Path[len("/api/users/"):]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
userMux.RLock()
user, exists := users[id]
userMux.RUnlock()
if !exists {
http.Error(w, "User not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(user)
// 修复了delete之后,再修复PUT方法缺少加锁的问题
case http.MethodPut:
var updatedUser User
if err := json.NewDecoder(r.Body).Decode(&updatedUser); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
userMux.Lock() //新增加锁
updatedUser.ID = id
users[id] = updatedUser
userMux.Unlock() //解锁
json.NewEncoder(w).Encode(updatedUser)
return
case http.MethodDelete:
userMux.Lock() //修改加锁位置
defer userMux.Unlock() //简化解锁逻辑
if _, exists := users[id]; !exists {
userMux.Unlock()
http.Error(w, "User not found", http.StatusNotFound) //返回404
return
}
delete(users, id)
//userMux.Unlock() 错误
//w.WriteHeader(http.StatusNotFound) StatusNotFound返回404 应该返回204
w.WriteHeader(http.StatusNoContent) //返回204
return
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
下周改进
- 每日小目标,不强求完美
- 先完成再优化
- 每天记录进展