「完整FX」Go从入门到进阶,大厂案例全流程实践

84 阅读6分钟

Go从入门到进阶,大厂案例全流程实践

实战 Go:构建简单分布式系统

分布式系统设计思路

在编写代码之前,我们先来确定需要实现的功能:

  • 主节点(Master Node):作为中控系统,负责派发任务命令。
  • 工作节点(Worker Node):作为执行者,负责执行任务。

此外,系统还需要实现以下功能:

  • 上报运行状态(Report Status):工作节点向主节点上报其当前状态。
  • 分派任务(Assign Task):通过 API 请求,主节点向工作节点分派任务。
  • 运行脚本(Execute Script):工作节点执行任务中的脚本。

实战步骤

节点通信

节点间的通信是分布式系统的核心。我们选择使用 gRPC 协议,它是一种流行的远程过程调用(RPC)框架,允许调用者在远程机器上执行命令。

  1. 创建 go.mod 文件并添加依赖:

    module go-distributed-system
    // go 1.17
    require (
      github.com/golang/protobuf v1.5.0
      google.golang.org/grpc v1.27.0
      google.golang.org/protobuf v1.27.1
    )
    
  2. 使用 Protocol Buffers 创建 node.proto 文件定义 gRPC 协议:

    syntax = "proto3";
    package core;
    option go_package = ".;core";
    
    message Request {
      string action = 1;
    }
    
    message Response {
      string data = 1;
    }
    
    service NodeService {
      rpc ReportStatus(Request) returns (Response){}; // Simple RPC
      rpc AssignTask(Request) returns (stream Response){}; // Server-Side RPC
    }
    
  3. 使用 protoc 命令将 .proto 文件转换为 Go 代码:

    mkdir core
    protoc --go_out=./core \
            --go-grpc_out=./core \
            node.proto
    

gRPC 服务端实现

  1. 创建 core/node_service_server.go 文件,实现 gRPC 服务端逻辑:
    package core
    
    import (
      "context"
    )
    
    type NodeServiceGrpcServer struct {
      UnimplementedNodeServiceServer
    
      // channel to receive command
      CmdChannel chan string
    }
    
    func (n NodeServiceGrpcServer) ReportStatus(ctx context.Context, request *Request) (*Response, error) {
      return &Response{Data: "ok"}, nil
    }
    
    func (n NodeServiceGrpcServer) AssignTask(request *Request, server NodeService_AssignTaskServer) error {
      for {
        select {
        case cmd := <-n.CmdChannel:
          // receive command and send to worker node (client)
          if err := server.Send(&Response{Data: cmd}); err != nil {
            return err
          }
        }
      }
    }
    
    var server *NodeServiceGrpcServer
    
    // GetNodeServiceGrpcServer singleton service
    func GetNodeServiceGrpcServer() *NodeServiceGrpcServer {
      if server == nil {
        server = &NodeServiceGrpcServer{
          CmdChannel: make(chan string),
        }
      }
      return server
    }
    

主节点实现

  1. 创建 node.go 文件,实现主节点逻辑:
    package core
    
    import (
      "github.com/gin-gonic/gin"
      "google.golang.org/grpc"
      "net"
      "net/http"
    )
    
    // MasterNode is the node instance
    type MasterNode struct {
      api     *gin.Engine           // api server
      ln      net.Listener          // listener
      svr     *grpc.Server          // grpc server
      nodeSvr *NodeServiceGrpcServer // node service
    }
    
    func (n *MasterNode) Init() (err error) {
      // grpc server listener with port as 50051
      n.ln, err = net.Listen("tcp", ":50051")
      if err != nil {
        return err
      }
    
      // grpc server
      n.svr = grpc.NewServer()
    
      // node service
      n.nodeSvr = GetNodeServiceGrpcServer()
    
      // register node service to grpc server
      RegisterNodeServiceServer(n.svr, n.nodeSvr)
    
      // api
      n.api = gin.Default()
      n.api.POST("/tasks", func(c *gin.Context) {
        // parse payload
        var payload struct {
          Cmd string `json:"cmd"`
        }
        if err := c.ShouldBindJSON(&payload); err != nil {
          c.AbortWithStatus(http.StatusBadRequest)
          return
        }
    
        // send command to node service
        n.nodeSvr.CmdChannel <- payload.Cmd
    
        c.AbortWithStatus(http.StatusOK)
      })
    
      return nil
    }
    
    func (n *MasterNode) Start() {
      // start grpc server
      go n.svr.Serve(n.ln)
    
      // start api server
      _ = n.api.Run(":9092")
    
      // wait for exit
      n.svr.Stop()
    }
    
    var node *MasterNode
    
    // GetMasterNode returns the node instance
    func GetMasterNode() *MasterNode {
      if node == nil {
        // node
        node = &MasterNode{}
    
        // initialize node
        if err := node.Init(); err != nil {
          panic(err)
        }
      }
    
      return node
    }
    

工作节点实现

  1. 创建 core/worker_node.go 文件,实现工作节点逻辑:
    package core
    
    import (
      "context"
      "google.golang.org/grpc"
      "os/exec"
    )
    
    type WorkerNode struct {
      conn *grpc.ClientConn // grpc client connection
      c    NodeServiceClient // grpc client
    }
    
    func (n *WorkerNode) Init() (err error) {
      // connect to master node
      n.conn, err = grpc.Dial("localhost:50051", grpc.WithInsecure())
      if err != nil {
        return err
      }
    
      // grpc client
      n.c = NewNodeServiceClient(n.conn)
    
      return nil
    }
    
    func (n *WorkerNode) Start() {
      // log
      fmt.Println("worker node started")
    
      // report status
      _, _ = n.c.ReportStatus(context.Background(), &Request{})
    
      // assign task
      stream, _ := n.c.AssignTask(context.Background(), &Request{})
      for {
        // receive command from master node
        res, err := stream.Recv()
        if err != nil {
          return
        }
    
        // log command
        fmt.Println("received command: ", res.Data)
    
        // execute command
        parts := strings.Split(res.Data, " ")
        if err := exec.Command(parts[0], parts[1:]...).Run(); err != nil {
          fmt.Println(err)
        }
      }
    }
    
    var workerNode *WorkerNode
    
    func GetWorkerNode() *WorkerNode {
      if workerNode == nil {
        // node
        workerNode = &WorkerNode{}
    
        // initialize node
        if err := workerNode.Init(); err != nil {
          panic(err)
        }
      }
    
      return workerNode
    }
    

封装主程序

  1. 创建 main.go 文件,封装主程序逻辑:
    package main
    
    import (
      "go-distributed-system/core"
      "os"
    )
    
    func main() {
      nodeType := os.Args[1]
      switch nodeType {
      case "master":
        core.GetMasterNode().Start()
      case "worker":
        core.GetWorkerNode().Start()
      default:
        panic("invalid node type")
      }
    }
    

运行和测试

  1. 启动主节点:

    go run main.go master
    
  2. 启动工作节点:

    go run main.go worker
    
  3. 通过 API 发送任务:

    curl -X POST \
      -H "Content-Type: application/json" \
      -d '{"cmd": "touch /tmp/hello-distributed-system"}' \
      http://localhost:9092/tasks
    
  4. 检查文件是否生成:

    ls -l /tmp/hello-distributed-system
    

构建完备的Golang知识体系

1. Golang 语言概述

Go(Golang)是由Google开发的编程语言,具备静态强类型、编译型、并发性,并且内置垃圾回收功能。Go的语法与C语言相似,但在变量声明上有所区别。Go语言并未包含C++的一些特性,如枚举、异常处理、继承等,而是提供了切片(Slice)、并发、管道、垃圾回收和接口等语言级特性。

2. Golang学习资料

  • 文章:理解Go语言特性。
  • 书籍:系统学习Go编程。
  • 作者论文:了解Go设计的理念。
  • 理论分析:掌握Go的内部机制。
  • 开源框架:通过实践学习Go应用。
  • 云原生技术:探索Go在云服务中的应用。
  • 专家视频:观看业界专家讲解。
  • 大厂实战分享:学习业界最佳实践。

3. Go语言的应用场景

Go语言设计用于系统编程,特别适合于Web服务器、存储集群等大型中央服务器。在高性能分布式系统领域,Go提供了高效的开发效率和海量并行支持,非常适合游戏服务端开发。

4. Golang适用的开发领域

  • 云计算基础设施:Docker、Kubernetes等。
  • 基础后端软件:TiDB、InfluxDB等。
  • 微服务架构:go-kit、micro等。
  • 互联网基础设施:以太坊、Hyperledger等。

5. Golang的局限性

  • 包管理:依赖于GitHub,由个人账户维护,存在风险。
  • 泛化类型缺失:Go 2.0计划中将加入。
  • 异常处理:所有异常通过Error处理。
  • C语言互操作性:与C的互操作并非无缝,存在序列化问题。

6. Golang CPU性能分析

6.1 程序运行时间分析

使用time指令(Linux系统)分析程序运行时间,包括:

  • real:程序实际经过的时间。
  • user:程序在用户CPU上的时间。
  • sys:程序在系统CPU上的时间。

一般real时间大于usersys之和。

/usr/bin/time -v go run a.go
6.2 Golang CPU性能分析工具
  • 引入net/http/pprof包并开启监听:

    import _ "net/http/pprof"
    http.ListenAndServe("0.0.0.0:10000", nil)
    

    通过浏览器访问http://127.0.0.1:10000/debug/pprof/查看CPU信息。

  • 使用go tool pprof命令分析性能数据:

    go tool pprof [binary] [profile]
    top //查看当前profile文件的cpu使用率
    

7. Golang程序内存分析方法

7.1 内存占用情况

使用top -p $(pidof 进程名)命令查看进程的内存占用情况。

7.2 使用GODEBUG和gctrace分析内存使用

设置GODEBUG='gctrace=1'来输出GC数据,分析垃圾回收时间和内存使用情况。

7.3 runtime.MemStats调试内存占用

定义runtime.MemStats对象来查看内存状态。

7.4 pprof分析内存

在程序中添加web端口监听,并通过http://127.0.0.1:10000/debug/pprof/heap?debug=1查看内存信息。

8. Golang面试题知识点总结

  • 函数返回值:多返回值时,至少一个有名称,其他也需命名。
  • 结构体比较:只有相同类型和属性顺序的结构体才可比较。
  • String与Nil类型:空字符串""表示String的空值,不能将nil赋值给String。
  • 常量:数据类型是固定内存大小的别名,用于编译器预算内存空间。
  • 内存四区:栈区、堆区、全局区、代码区,各自有不同的特点和用途。
  • 数组与切片:讨论了切片的初始化、追加、拼接和使用new的问题。
  • Map:讨论了Map的value赋值和遍历问题。
  • Interface:讨论了interface的赋值、内部构造和空接口问题。
  • Channel:总结了Channel可能出现的特殊情况。
  • WaitGroup:提供了WaitGroup的使用图解。