双非大三自救:自学Go语言冲刺大厂Offer

0 阅读14分钟

前言:大家好啊,我是绣虎,一名来自双非的大三学生,这是我发的第一篇博客。由于上大学之后就一直沉迷于电脑游戏,生活和学习相去甚远,可谓是宁可参加田径队、篮球队、辩论赛也是不愿意静下心来好好学习,导致的结果就是又是挂科又是重修,所幸绩点还算稳在能拿学位证的程度。

而在某一天的清晨,站在校园中,感觉自己和大家欣欣向荣的氛围格格不入时,突然一阵心潮涌动,仿佛感受到了什么,瞧着自己日益苍老的父母,是否我也该为未来考虑一下了,俗话说浪子回头金不换,想到什么先做就完事了,于是就有了这个计划:我要通过学习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基础语法

  1. Go语言是一门灵活的语言,在它的身上我看到了很多java的影子,这也是我决定以Go为主以java为辅的原因之一。

  2. 变量的定义 变量分为:局部变量全局变量
    局部变量 一经定义就必须要使用,不然就报错,局部变量有四种定义方法
    全局变量 定义在方法体外,类之内,整个类的方法都能用这个变量

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"


  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)
}
  1. 基本数据类型

int类型

  1. 默认的数字定义类型是int类型
  2. 带个u就是无符号,只能存正整数
  3. 后面的数字就是2进制的位数(uint8:8位无符号二进制)
  4. uint8还有个别名是byte,一个字节=8个bit位
  5. int类型的大小取决于所使用的平台,所以为了易用性,还是声明位数较好
  6. 示例:
    • 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,可以计算小数位

字符型

  1. 掌握byte(单字节字符)和rune(多字节字符)即可,前者适用非汉语体系字符,后者适用汉语体系字符。
  2. 在Go中,字符的本质是一个整数,直接输出时,是该字符对应的UTF-8编码的码值。
  3. 可以直接给某个变量赋一个数字,然后按照格式化输出时%c,会输出该数字对应的unicode字符。
  4. 字符类型是可以进行运算的,因为它们相当于一个整数,都有各自对应的unicode码。

字符串类型

和字符类型不一样的是,字符类型赋值用的是单引号,字符串用双引号

转义字符 记住常用的就好

  1. \t:制表符(空格)
  2. \n:回车
  3. \"内容\":添加双引号
  4. \r:回到行首(前面输入的都清空)
  5. \\内容\\:反斜杠(让内容两旁出现斜杠)

布尔类型

布尔类型只有真(true)假(false)两个值

  1. 默认值为false
  2. Go语言中不允许将整型类型转为布尔类型
  3. 布尔类型无法参与数值运算,也无法与其他类型进行转换

零值问题

如果一个基本数据类型只声明不赋值,那么这个变量的值就是对应类型的零值

  1. int:0

  2. bool:false

  3. string:""

  4. float32:0

  5. pointer:nil

数据结构:结构体、切片、映射

结构体

结构体定义需要用type和struct语句。struct语句定义一个新的数据类型,结构体中有一个或多个成员。type语句设定了结构体的名称。

切片

Go语言切片是对数组的抽象,Go数组的长度不可改变,在某些场景这样的集合就不太适用,这里提供一个新的更为灵活的内置类型切片(“动态数组”),可以不断追加元素,在追加时会根据是否越界使切片的容量增大

映射

较为复杂,所以略加一些详细的内容po上来:

  • 关键特性:引用类型、无序、key唯一、必须初始化
  • 核心操作:赋值、取值、遍历、删除、判存在
  1. Map 是一种无序的键值对的集合。

  2. Map 最重要 的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

  3. Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,遍历 Map 时返回的键值对的顺序是不确定的。

  4. 在获取 Map 的值时,如果键不存在,返回该类型的零值,例如 int 类型的零值是 0,string 类型的零值是 "" 。

  5. Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 Map 的修改会影响到所有引用它的变量。

  6. 必背:

    1. map是引用类型 , 必须用 make 初始化才能赋值

    2. Key 必须可比较 (不能是切片/函数/集合), value任意类型

    3. 取值一定要用 value, ok := map[key] 判断 key 是否存在

    4. 遍历无序, 不要依赖遍历顺序

    5. 无内置清空函数,直接make新map即可

HTTP处理函数

在Go语言中,HTTP处理函数是用于处理HTTP请求的核心组件:

基本处理函数

// HandlerFunc 类型
type HandlerFunc func(ResponseWriter, *Request)

// Handler 接口
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

创建处理函数

  1. 方式一:普通函数
func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}

// 注册
http.HandleFunc("/hello", helloHandler)
  1. 方式二:实现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)

常用处理模式

  1. 带参数的路径
func userHandler(w http.ResponseWriter, r *http.Request) {
    // 获取路径参数
    vars := mux.Vars(r) // 使用 gorilla/mux
    id := vars["id"]
    fmt.Fprintf(w, "User ID: %s", id)
}
  1. 处理不同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 数据类型的互转(编 / 解码)

  1. 编码(Marshal/Encode)

    1. 定义:将 Go 结构体(struct)、map 等类型转换为 JSON 字符串(字节流 []byte)

    2. 常用方式

      1. json.Marshal(v):直接将数据序列化为 []byte
      2. json.NewEncoder(w).Encode(v):将数据直接编码并写入 io.Writer(如 HTTP 响应体 http.ResponseWriter)
    3. 规则

      1. 结构体字段必须首字母大写才能被导出(被 json 包访问)
      2. 标签 json:"name" 指定 JSON 中的字段名,如 Status string \json:"status"``
      3. omitempty:如果字段值为空(0 值、nil、空字符串),序列化时会忽略该字段
  2. 解码(Unmarshal/Decode)

    1. 定义:将 JSON 字符串反序列化为 Go 的结构体或 map

    2. 常用方式

      1. json.Unmarshal(data, &v):从字节流解析到变量

      2. json.NewDecoder(r).Decode(&v):从 io.Reader(如 HTTP 请求体 r.Body)中读取并解析

    3. 空接口 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)
	}
}

下周改进

  • 每日小目标,不强求完美
  • 先完成再优化
  • 每天记录进展