本指南面向:
前端开发人员
为什么现在市场中,拥有服务端开发经验的前端更具有竞争力?因为卷工作台应用,熟悉前端框架,组件库,打包的可替代性太高了,那如何提高竞争力呢?跨端,可视化,复杂组件,或者?掌握一门服务端技能?因为前端开发缺乏后端开发经验,可能会导致在设计和实现功能时遇到问题,尤其是在涉及到服务器端的逻辑和数据存储时失去对整个系统设计的知情权。
后端开发人员 K8S是7年前发布的,今天已经被称为后云原生时代,作为云原生时代的优势语言,Golang 简单,高效,加上现在社区的发展,Golang 可以轻松实现各种需求和任务。学习Golang可以让后端开发人员更加有竞争力,并且可以更好地处理高负载和高并发的场景。
第一天 学会使用 golang 写API
开始之前可以先下载 Go Land(免费试用30天),火速初始化你的 Golang 开发环境。
启动 http 服务器
假设你已经拥有了一个 golang 开发环境,你需要了解的是: 入口文件是 main.go
在入口文件中,需要标明这个是 main
包
css
// main.go
package main
接着启动一个 HTTP 服务器,监听 3000 端口,接收第一个请求并返回 hello world
golang
import (
"net/http"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("<h1>Hello World!</h1>"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
http.ListenAndServe(":"+ 3000, mux)
}
Line 10
的 ServeMux 是一个 multiplexer 请求多路复用器,作用就是注册路由和对应的处理函数通过 mux.HandleFunc
注册,比如访问 url /api 则调用 handleAPI 函数处理, 类似一个 map
arduino
{
'/api': handleAPI
'/': indexHandler
}
Line 5-7
是一个请求处理函数,当有人请求 /
时,w
是向客户端返回数据用的,r
是获取请求信息的,比如获取 reuqest header 中前端传过来的 token 字段就可以用 r.Header.Get("token")
。
更进一步,需求是 访问 /
时,将请求头中的 token
以 json格式返回给它,格式如下:
js
{
token: "xxxxxx"
}
获取请求信息与返回数据
我们知道如何获取 header 中的token, 也知道如何返回数据,那如何返回 json 格式的数据呢?我们需要先引入 encoding/json
包来生成 json 格式, 最后通过设置返回格式为 json即可。
golang
import (
"encoding/json"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("token")
response := map[string]string{"token": token}
jsonResponse, err := json.Marshal(response)
if err != nil {
// 处理错误
}
w.Header().Set("Content-Type", "application/json")
w.Write(jsonResponse)
}
Line 7
是定义了一个 map,key 为string, value 也为 string, 并且赋值为
json
// response
{
"token": "xxxxxxxx"
}
然后通过 json.Marshal
的方法将这个 map 转换为 json 字符串格式,最后通过 w.Write返回客户端 json 数据。
来测试一下
环境变量
目前我们程序中 port
是写死 3000的,通常我们会将这种配置信息,写到环境配置文件 .env
中,然后程序从环境变量中读取。
新建个 .env
文件写个 PORT 配置
js
PORT=3000
引入github.com/joho/godotenv
包来将本地的 .env 文件读取到环境变量中, 可以用 go get下载包
arduino
go get github.com/joho/godotenv
也可以用 Go Land的点一下同步依赖功能
小Tips:如果网络有问题设置一下国内代理
sh
$ go env -w GO111MODULE=on
$ go env -w GOPROXY=https://goproxy.cn,direct
golang
err := godotenv.Load()
if err != nil {
log.Println("Error loading .env file")
}
然后把之前写死的监听端口号改改
golang
port := os.Getenv("PORT")
http.ListenAndServe(":"+port, mux)
更舒服的开发,设置监听文件改动重启
能跟到这里的话我们就发现,每次修改代码,还要重新启动,怪麻烦的,如何设置监听文件变更重新启动呢? 下载一个air通过
golang
go install github.com/cosmtrek/air@latest
然后在项目根路径下运行 air
即可
发起请求
前面我们已经获取到了 token,让我们判断一下,如果没有token 就返回 403 用户没有权限,我们暂时先不判断 token 的有效性,即有 token 才能 call 我们的接口
go
func indexHandler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("token")
if "" == token {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
err := json.NewEncoder(w).Encode(map[string]interface{}{
"message": "your token invalid",
})
if err != nil {
return
}
}
像 Line 6 的 err :=
这种语法在 golang 中随处可见,这是用来做错误处理的,就类似 JS 中的 try catch
语法。
js
try{
JSON.parse(xxx)
}catch(e){
// 错误处理
}
json.NewEncoder(w).Encode(map[string]interface{}{ "message": "your token invalid", })
是返回给客户端 JSON 数据
go
{
"message": "your token invalid",
}
map[string]interface{}
是一个键值对的集合,其中键是字符串类型,值是任意类型。这种数据结构允许我们以动态的方式存储不同类型的数据。后面的那个{xxx:xxx}
是来初始化数据。
做完 token 验证后,我们来实现这个接口的核心功能 通过名字预测性别,这里我们需要 call 一个第三方的接口来实现这个功能。
go
name := r.URL.Query().Get("name")
if name == "" || !isString(name) {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "name参数必须是字符串")
return
}
url := fmt.Sprintf("https://api.genderize.io?name=%s", name)
response, err := http.Get(url)
if err != nil {
fmt.Println("请求失败:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer response.Body.Close()
var result map[string]interface{}
err = json.NewDecoder(response.Body).Decode(&result)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "success",
"data": result,
})
// isString实现
func isString(s string) bool {
for _, c := range s {
if c < 'a' || c > 'z' {
return false
}
}
return true
}
Line 1 从客户端的 url 上取出来 name
, 取出来后通过 fmt.Sprintf
将 name 拼接到请求地址上,也可以使用 +
号拼接。 拼好后通过 http.Get
就可以 call 第三方 API了,使用defer
关键字可以确保无论函数是否发生错误,都会执行Close()方法来关闭响应主体, 在处理HTTP请求时,我们应该始终在使用完响应主体后调用response.Body.Close()方法来关闭它,以释放相关的资源, 避免内存泄漏。
Line 3 校验了一下客户端的输入,谨记,在任何时候都要去对用户的输入进行验证!
JSON 在网络中传输是字符串格式, 在 Line 21 我们将第三方 API 返回的 JSON 字符串格式转换为 Go 中的 JSON 对象,即 map[string]interface{} 通过 json 库的 Decoder。 最后我们重新包装了一下返回格式给客户端。
现在来试一下:
不传 token 时,会提示 403 错误
请求接口不带 name 时, 会提示 400 客户端错误
当请求参数都正确时,返回我们想要的结果
部署到 K8S
Golang 的部署极其简单,比如下面的 Dockerfile, 而 Node.js 或者前端镜像则需要安装依赖,并在构建过程中执行相应的构建命令,如 npm install, 而且大部分情况还要分阶段构建。相比 Node.js 的CI/CD, Golang 的运维心智负担很低。
打镜像
bash
# 使用官方 Golang 镜像作为基础镜像
FROM golang:latest
# 设置工作目录
WORKDIR /app
# 将当前目录下的所有文件复制到工作目录
COPY . .
# 构建 Go 应用程序
RUN go build -o main .
# 定义容器启动时运行的命令
CMD ["./main"]
通过运行 docker build -t my-golang-app .
生成我们的程序镜像
在k8s上运行镜像
先创建k8s部署文件
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-golang-app
spec:
replicas: 1
selector:
matchLabels:
app: my-golang-app
template:
metadata:
labels:
app: my-golang-app
spec:
containers:
- name: my-golang-app
image: my-golang-app:latest
ports:
- containerPort: 8080
然后运行 kubectl apply -f deployment.yaml
将这个容器跑在 K8S 上, 现在我们该给这个容器一个 IP 地址,让我们能够访问它
yaml
apiVersion: v1
kind: Service
metadata:
name: my-golang-app-service
spec:
selector:
app: my-golang-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
然后运行kubectl apply -f service.yaml
, 就会创建一个 my-golang-app-service
,它将流量转发到 my-golang-app
部署的容器的端口 8080。这时我们可以使用 Kubernetes 集群的 IP 地址或域名和服务的端口号来访问应用程序。
例如,如果你的 Kubernetes 集群的 IP 地址为 192.168.0.100
,你可以通过浏览器访问 http://192.168.0.100:80
来访问 my-golang-app
。
当然你也可以通过创建一个 ingress,绑定一个证书通过域名来访问,这里细心的同学会发现,我们之前写了一个配置文件 .env, 他是需要挂载进来的,让我们稍微修改一下前面的 deployment 文件
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-golang-app
spec:
replicas: 1
selector:
matchLabels:
app: my-golang-app
template:
metadata:
labels:
app: my-golang-app
spec:
containers:
- name: my-golang-app
image: my-golang-app:latest
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: my-golang-app-configmap
---
apiVersion: v1
kind: ConfigMap
metadata:
name: my-golang-app-configmap
data:
.env: |
KEY1=value1
KEY2=value2
如果看不懂的没有关系,跳过这部分,我们后面会讲的更详细,更生产!
加日志
程序部署以后没日志不行,我们都不知道有没有人访问我们的应用程序,怎么加日志呢?假设我们有 10 个接口, 我们需要给每个接口都加一个访问日志吗? 不需要的,我们可以利用中间件,中间件是一种常见的设计模式,用于在处理请求之前和之后执行一些共享的逻辑, 常用于处理身份验证、日志记录、错误处理等功能。
举个例子
go
package main
import (
"fmt"
"net/http"
)
// 中间件函数
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 在处理请求之前执行的逻辑
fmt.Println("Executing middleware before request")
// 调用下一个处理程序
next.ServeHTTP(w, r)
// 在处理请求之后执行的逻辑
fmt.Println("Executing middleware after request")
})
}
// 处理请求的处理程序
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Println("Executing handler")
fmt.Fprintf(w, "Hello, World!")
}
func main() {
// 创建路由处理器
router := http.NewServeMux()
// 注册中间件
router.Handle("/", middleware(http.HandlerFunc(handler)))
// 启动服务器
http.ListenAndServe(":8080", router)
}
从 Line 33 行来看,middleware 就是一个 wrapper, 它包裹了一下原来的路由处理函数,典型的AOP 应用场景,当请求来,先调用我们的 middleware 来替代原来的 handler, 所以你看到 Line 10 也是一个 http handler,调用我们后,我们就可以先执行一些前置操作,然后再调用原来的 handler,原来的handler运行完,再次到 middleware 来运行一些后置操作。即所谓的"洋葱模型"
洋葱芯是包裹的 handler,我们可以在外面套很多个中间件
接下来就写个 logger 中间件,包一下每个handler(虽然我们只有一个。。。),让每个接口都可以记录访问。
go
func logging(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(r.URL.Path)
f(w, r)
}
}
mux.HandleFunc("/", logging(indexHandler))
结束语
第一天结束了,看到这里了,点个赞吧!第一次学习 golang 的同学一定要搭个环境,手动练习一下,很多概念对于第一次接触很陌生,看不懂就查是最高效的学习方式(避免漫无目的的学习)。本系列教程循序渐进,巩固一下第一天的课程,让我们期待第二天。