Go 项目(一)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

基础

  • 基础部分参考这个系列
  • 接下来的这部分是对上面的更新和重构更加深入理解框架部分

环境

  • 基础环境主要在Linux上搞最主要是 dockerdocker-composenode记得 docker 要配置阿里云镜像加速器
    1
    # docker
    curl -fsSL https://get.docker.com bash -s docker --mirror Aliyun
    sudo systemctl start docker
    sudo systemctl enable docker
    sudo usermod -aG docker ${USER}
    sudo chmod a+rw /var/run/docker.sock
    sudo systemctl restart docker
    docker run hello-world
    
    3
    4
    # mysql
    docker run -p 3306:3306 --name mymysql -v $PWD/conf:/etc/mysql/conf.d -v $PWD/logs:/logs -v $PWD/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7
    mysql -uroot -p
    # 授权访问
    GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'root' WITH GRANT OPTION;
    GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' IDENTIFIED BY 'root' WITH GRANT OPTION;
    GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY 'root' WITH GRANT OPTION;
    FLUSH PRIVILEGES;
    
  • centos7上安装 node推荐使用16.16.0版本太高会报错
    2

包管理

  • 说明一下 go path 和现在 go module 之间的差异
  • GOPATH
    • go env 能看到这个需要自己设置比如 D:\Go\workspace;
    • 那么我们新建的项目都要放在 D:\Go\workspace\src 下面
    • 同时要设置 GO111MODULE=offgo env -w
    • 不设置的话会去 GOROOT 下面找就是安装目录 D:\Go\src 下的放标准库的
    • 也就是说这种包管理方式就是不做包管理对开发go的人容易对go开发者很头疼
  • go module
    • 设置 GO111MODULE=on
    • 在 GoLand 新建项目会自动新建 go.mod 文件有这么两行
      module goModProject
      go 1.17
      
    • import 未下载的包会自动下载并管理可以多版本

编码规范

  • 代码规范不是强制的⽬的是⽅便团队形成⼀个统⼀的代码⻛格提高可读性规范并不是唯⼀的

命名规范

  • 统⼀的命名规则有利于提⾼的代码的可读性仅通过命名就可以获取到⾜够多的信息
  • 大写字⺟开头常量、变量、类型、函数名、结构体等代表可以被外部包的代码所使用也被称为导出
  • 小写字⺟开头则对包外是不可见的类似 Java 的 private
  • 包名package
    • 保持 package 的名字和⽬录⼀致尽量采取有意义的包名和标准库不要冲突
    • 使用小写单词不要使⽤下划线或者混合大小写
  • 文件名
    • 应该为小写单词使⽤下划线分隔各个单词
  • 结构体
    • 采用驼峰命名法首字母根据访问控制⼤写或者小写
      type User struct{
      	Username string
      	Email   string
      }
      u := User{
      	Username: "Roy",
      	Email:  "roy.yang@gmail.com",
      }
      
  • 接口
    • 和结构体类似
    • 单个函数的接口名以 er 作为后缀
      type Reader interface {
          Read(p []byte) (n int, err error)
      }
      
  • 变量
    • ⼀般遵循驼峰法
    • 遇到特有名词时APIID等需要遵循以下规则
      • 如果变量为私有且特有名词为首个单词则使用小写如 apiClient
      • 其它情况都应当使⽤该名词原有的写法如 APIClient、repoID、UserID错误示例UrlArray
      • 若变量类型为 bool 类型则名称应以 Has, Is, Can 或 Allow 开头
  • 常量命名
    • 常量均需使⽤全部⼤写字⺟组成并使⽤下划线分词
    • 如果是枚举类型的常量需要先创建相应类型
      const  APP_VER = "1.0.0"
      type Scheme string
      const (
      	HTTP Scheme = "http"
      	HTTPS Scheme = "https"
      )
      

注释

  • 清晰的注释非常重要
  • 虽然我们自己不喜欢写注释但是很讨厌不写注释的人
  • go 的注释是 C++ 风格用 // 单行注释和 /**/ 多行注释
  • go 语⾔⾃带的 godoc ⼯具可以根据注释生成⽂档
  • 包注释
    • 每个包都应该有⼀个包注释⼀个位于 package 子句之前的块注释或行注释
    • 应该包含
      /*
      包的基本简介包名简介
      创建者格式 创建⼈ rtx 名
      创建时间格式创建时间 yyyyMMdd
      */
      
  • 结构体/接口注释
    • 结构体名 结构体说明
    • 结构体内的每个成员变量都要有说明该说明放在成员变量的后⾯
  • 函数注释
    • 应包括三个方面
      /*
      简要说明格式说明以函数名开头“” 分隔说明部分
      参数列表每⾏⼀个参数参数名开头“” 分隔说明部分
      返回值 每⾏⼀个返回值
      */
      
    • 上面说的这些注释都是写在上面
  • 代码注释
    • ⼀些关键位置的代码逻辑或者局部较为复杂的逻辑需要有相应的说明
  • 注释风格
    • 中英⽂字符之间严格使⽤空格分隔

import 规范

  • 包分三类标准库包程序内部包第三⽅包
  • 建议采用如下顺序
    import (
    	"encoding/json"
    	"strings"
    	
    	"github.com/astaxie/beego"
    	"github.com/go-sql-driver/mysql"
    	
    	"myproject/models"
    	"myproject/controller"
    	"myproject/utils"
    )
    
  • 在项⽬中不要使⽤相对路径引⼊第三方包本项目自己的包建议用相对路径

错误处理

  • 不能丢弃任何返回 err 的调⽤不要使⽤ _ 丢弃必须全部处理
  • 接收到错误要么 return err要么用 log 记录下来
  • 尽量不要用 panic除非你知道你在做什么
  • 采⽤独⽴的错误流进⾏处理
    // 错误写法
    if err != nil {
     	// error handling
    } else {
    	// normal code
    }
    
    // 正确写法
    if err != nil {
    	// error handling
    	return	// or continue
    }
    

RPC

  • remote procedure call 一个节点请求另一个节点的服务一般分布式应用才会用到
  • 将本地过程调用变为远程过程调用要面临的问题我们可以类比本地调用过程
    • Call 的 ID 映射
    • 序列化和反序列化json/xml/protobuf/msgpack
    • 网络传输gin/beego/net
  • 前面解释过这些概念和流程这里再举个例子回顾一下
    • 先看一个简单的RPC流程
      1
    • 调用端和服务端都要经历序列化和反序列化
    • 我们选择 protobuf 作为序列化协议更为通用不同语言之间只要遵守了协议也能交流
    • json 序列化主要是效率不够高一般只在 gin 和浏览器之间使用但我们的服务不止在这两个地方
    • 网络传输也是必不可少的我们需要建立 http 连接在 grpc 中使用 http2.0 避免连接断开的问题我们也可以自己封装一层协议
  • 使用 RPC
    • 封装一个简单的 RPC 流程
    • server 端使用 http 启动服务
      // server/main.go
      /*
      1.定义处理逻辑反序列化序列化
      2.启动服务监听端口
      */
      package main
      
      import (
      	"encoding/json"
      	"fmt"
      	"net/http"
      	"strconv"
      )
      
      func main() {
      	// http://127.0.0.1:8000?a=1
      	http.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request) {
      		_ = r.ParseForm() // 解析参数
      		fmt.Println("path:", r.URL.Path)
      		a, _ := strconv.Atoi(r.Form["a"][0]) // 搞成int
      		w.Header().Set("Content-Type", "application/json")
      		d, _ := json.Marshal(map[string]int{"data": a}) // 序列化格式化
      		_, _ = w.Write(d)
      	})
      	err := http.ListenAndServe(":8088", nil)
      	if err != nil {
      		fmt.Println(err)
      	}
      }
      
    • client 端网络传输http序列化jsonID/add
      // client/main.go
      /*
      1.建立连接
      2.序列化数据
      3.发起请求
      4.反序列化返回值
      */
      package main
      
      import (
      	"encoding/json"
      	"fmt"
      	"github.com/kirinlabs/HttpRequest"
      )
      
      type RespData struct {
      	Data int `json:"data"` // 使用json解析
      }
      
      func Add(a int) int {
      	req := HttpRequest.NewRequest()
      	res, _ := req.Get(fmt.Sprintf("http://127.0.0.1:8088/%s?a=%d", "add", a))
      	body, _ := res.Body()
      	//fmt.Println(string(body)) // {"data":1}
      	r := RespData{}
      	_ = json.Unmarshal(body, &r)
      	return r.Data
      }
      func main() {
      	fmt.Println(Add(10))
      }
      
    • 看起来和普通的单体应用前后端请求没啥区别
    • 因为这里在本地请求主要是体会 RPC 的几个要点后面 grpc 框架会帮我们解决这些问题
  • RPC 框架开发要素
    • 技术架构上有四部分客户端、客户端存根、服务端、服务端存根
      3
    • 存根stub干大部分的活
      • 该程序运⾏在客户端所在的计算机机器上主要⽤来存储要调⽤的服务器的地址
      • 另外还负责将客户端请求远端服务器程序的数据信息打包成数据包通过⽹络发送给服务端Stub 程序
      • 其次还要接收服务端 Stub 程序发送的调⽤结果数据包并解析返回给客户端
    • 具体开发中我们都会使用动态代理技术⾃动⽣成的 Stub 程序
      • 因为地址、数据信息等可能会修改stub 文件也可能会非常多靠人写估计会辞职

内置 RPC

  • 尝试使用 go 语言内置 RPC记得要和上面的简单过程对比
  • server 端
    package main
    
    import (
    	"net"
    	"net/rpc"
    )
    
    // class
    type HelloService struct {
    }
    
    // class function 定义处理逻辑
    func (s *HelloService) Hello(request string, reply *string) error {
    	// 通过修改指针地址返回
    	*reply = "hello" + request // 注这里的 *reply 是不能分配空间的调用时必须实例化
    	return nil
    }
    
    func main() {
    	// 1.实例化server
    	listen, _ := net.Listen("tcp", ":1234")
    	// 2.注册处理逻辑
    	_ = rpc.RegisterName("HelloService", &HelloService{})
    	// 3.启动服务监听端口
    	conn, _ := listen.Accept()
    	rpc.ServeConn(conn)
    }
    
  • client 端
    package main
    
    import (
    	"fmt"
    	"net/rpc"
    )
    
    func main() {
    	// 1. 建立连接
    	client, err := rpc.Dial("tcp", "localhost:1234")
    	if err != nil {
    		panic(err)
    	}
    	// 2. 发起请求
    	// var reply = new(string) // new会实例化空间并返回指针通过指针reply返回数据这里必须分配空间不能传nil或者说不能直接写成 var reply *string
    	var reply string // 或者直接用 string 类型它是有默认值的或者说有地址不是 nil
    	err = client.Call("HelloService.Hello", "roy", &reply)
    	if err != nil {
    		panic("调用失败")
    	}
    	fmt.Println(reply) // 传用 &取用 *
    }
    
  • 上面的过程明显简单了很多本质是 rpc 把 ID 和序列化的操作搞定了无需我们关心底层细节

改协议

  • 上面的调用有些怪异但 go 是静态语言不像 python 能直接 client.Hello()
  • 我们可以跨语言发起请求但要先更换协议
  • go 用的是 Gob 协议换成 json
    // server
    package main
    
    import (
    	"net"
    	"net/rpc"
    	"net/rpc/jsonrpc"
    )
    
    // class
    type HelloService struct {
    }
    
    // class function
    func (s *HelloService) Hello(request string, reply *string) error {
    	// 通过修改指针地址返回
    	*reply = "hello" + request // 注这里的 *reply 是不能分配空间的调用时必须实例化
    	return nil
    }
    
    func main() {
    	// 实例化server
    	listen, _ := net.Listen("tcp", ":1234")
    	// 注册处理逻辑
    	_ = rpc.RegisterName("HelloService", &HelloService{})
    	// 启动服务监听端口
    	for {
    		conn, _ := listen.Accept()
    		// 使用 json 序列化
    		go rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) // 使用协程
    	}
    
    }
    
    // client
    package main
    
    import (
    	"fmt"
    	"net"
    	"net/rpc"
    	"net/rpc/jsonrpc"
    )
    
    func main() {
    	// 1. 建立连接
    	conn, err := net.Dial("tcp", "localhost:1234") // 不用 rpc 了否则还是 Gob 协议
    	if err != nil {
    		panic(err)
    	}
    	// 2. 发起请求
    	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) // 使用 json 序列化
    	var reply string
    	err = client.Call("HelloService.Hello", "roy", &reply)
    	if err != nil {
    		panic("调用失败")
    	}
    	fmt.Println(reply)
    }
    
  • 接下来用 python 发起请求
    • 这里可以用 socket特点是每次调用就会发一次消息等待套接字处理不像 requests 底层是 HTTP 协议需要建立连接
    • 方便起见我们还是用 requests先将 go 端的 server 改为使用 http 协议并启动
      package main
      
      import (
      	"io"
      	"net/http"
      	"net/rpc"
      	"net/rpc/jsonrpc"
      )
      
      // class
      type HelloService struct {
      }
      
      // class function
      func (s *HelloService) Hello(request string, reply *string) error {
      	// 通过修改指针地址返回
      	*reply = "hello" + request // 注这里的 *reply 是不能分配空间的调用时必须实例化
      	return nil
      }
      
      func main() {
      	// 1.实例化server
      	_ = rpc.RegisterName("HelloService", &HelloService{})
      	// 将 rpc 的传输协议改为 http先不用理解照猫画虎
      	http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
      		var conn io.ReadWriteCloser = struct {
      			io.Writer
      			io.ReadCloser
      		}{
      			ReadCloser: r.Body,
      			Writer:     w,
      		}
      		// ServeRequest is like ServeCodec but synchronously serves a single request.
      		// It does not close the codec upon completion.
      		rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
      	})
      	http.ListenAndServe(":1234", nil)
      }
      
    • python 端
      import requests
      
      # 这些key名称是固定的
      req = {
          "id": 0,
          "params": ["Roy"],
          "method": "HelloService.Hello"
      }
      
      rsp = requests.post("http://localhost:1234/jsonrpc", json=req)
      print(rsp.text) # {"id":0,"result":"helloRoy","error":null}
      
    • 但还是没有实现上面说的 client.Hello() 形式的调用可以参考这段代码理解
      import zerorpc
      
      c = zerorpc.Client()
      c.connect("tcp://127.0.0.1:1234")
      print(c.Hello("Roy"))
      
    • 在 python 中有一个魔法方法 __getattr__即使 client 端没有 Hello 方法的定义也能根据方法名称构建网络请求可以进入源码查看
  • 上面改了序列化协议、传输协议在 python 端尝试调用

改调用

  • 我们回到 go它没有 python 的高级特性只能自定义从这里开始我们朝 RPC 框架逼近了
  • 结构分析
    • 首先要有一个 handler 模块因为 client 和 server 可能不在一个服务器总不能为了运行 client 把 server 代码拿过去将处理逻辑放在 handler大家各自带上
      // handler/handler.go
      package handler
      
      // 名称冲突问题
      // 相当于 IDserver_proxy 中会注册这个 nameclient_proxy 中会根据这个 ID 调用注册的 Hello 方法
      const HelloServiceName = "handler/HelloService"
      
      type NewHelloService struct {
      }
      
      func (s *NewHelloService) Hello(req string, reply *string) error {
      	*reply = "Hello, " + req
      	return nil
      }
      
    • client 想直接调用 Hello还需要一层封装我们称为 client_proxy主要作用是与 server 建立连接封装出 Hello 方法给 client
      package client_proxy
      
      import (
      	"goModProject/helloworld/handler"
      	"net/rpc"
      )
      
      // 所有方法归在 Stub
      type HelloServiceStub struct {
      	*rpc.Client
      }
      
      // 这里是连接
      // go 语言没有类没有初始化方法
      func NewHelloServiceClient(protocol, address string) HelloServiceStub {
      	conn, err := rpc.Dial(protocol, address)
      	if err != nil {
      		panic("Connect Error!")
      	}
      	return HelloServiceStub{conn}
      }
      
      // 这里是调用具体的逻辑Hello方法
      func (c *HelloServiceStub) Hello(req string, reply *string) error {
      	// serviceMethod: Hello对就是 .Hello 调用
      	err := c.Call(handler.HelloServiceName+".Hello", req, reply)
      	if err != nil {
      		return err
      	}
      	return nil
      }
      
    • 同样将注册逻辑放在 server_proxy并使用接口作为参数解耦
      // server_proxy.go
      package server_proxy
      
      import (
      	"goModProject/helloworld/handler"
      	"net/rpc"
      )
      
      // 在服务端注册处理逻辑
      // 这么写耦合太紧了proxy 受限于 handler
      //func RegisterHelloService(srv handler.NewHelloService) error {
      //	return rpc.RegisterName(handler.HelloServiceName, srv)
      //}
      
      // 使用接口解耦
      type HelloService interface {
      	Hello(request string, reply *string) error
      }
      
      // 使用接口作为参数只要传入的结构体实现了 Hello 方法即可不需要指定 handler.NewHelloService
      // 即使 handler 中的结构体名字变了也不需要改这里
      // server 和 handler 中对上即可
      func RegisterHelloService(srv HelloService) error {
      	return rpc.RegisterName(handler.HelloServiceName, srv)
      }
      
      // server.go
      package main
      
      import (
      	"goModProject/helloworld/handler"
      	"goModProject/helloworld/server_proxy"
      	"net"
      	"net/rpc"
      )
      
      func main() {
      	// 实例化server
      	listen, _ := net.Listen("tcp", ":1234")
      	// 注册处理逻辑
      	_ = server_proxy.RegisterHelloService(&handler.NewHelloService{})
      	// 启动服务监听端口
      	for {
      		conn, _ := listen.Accept()
      		rpc.ServeConn(conn)
      	}
      }
      
  • 上面的过程一定要研究明白主要是结构上优化定义了框架的四要素底层三问题还是使用 RPC 解决
  • 问题也来了如果所有的 Stub 都自己写会痛苦不堪的
    • 但我们发现两个 proxy 的内容其实是有规律的也就意味着可以自动生成
    • 需要用到一个工具protoc
    • 后面从 grpc 开始介绍和我们这部分对比就会发现过程一样
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: go