在我编程生涯中也就使用过很多门编程语言,这其中就包括所谓的大道之间的 Go 语言,总体来说使用 Go 语言来编写逻辑是非常容易的,和天生支持 CSP 和 GMP 模型的高并发的语言,使得开发高并发应用也非常简单。它的语法简单和 25 关键字非常容易掌握,能使程序员快速掌握并且编写代码逻辑,并不意味着 Go 的设计有多么好,反而在我使用 Go 编程过程中出现很多感觉很丑陋的设计。

Go 程序的错误处理,例如:Go 错误处理,写多了满屏 if err != nil,这个和 C 语言的一脉相承的;

  if len(files) <= 0 {
        // 它这个错误设计和 JVM 异常相比很垃圾
        err := lfs.createActiveRegion()
        if err != nil {
            return err
        }
        // 可以直接这么写,但是会丢弃错误处理上下文
        // return lfs.createActiveRegion()
        return nil
  } 该如何编写这段代码逻辑

没有方法重载入,还有三目表达式,没有模式匹配,内置 init 函数和自定义 init 互相冲突并且自定义 var x = func() {}() 执行顺序和内置 init 函数执行顺序冲突,函数式编程没有 lambda 表达式的支持,范型不支持协变和逆变,标识符、Struct、变量、字段的访问控制和 JSON 正反序列化矛盾的冲突。另外构造器随意创建,不支持函数重载导致需要使用魔法编程可变参数进行实现类似于其他有重载功能语言,这对于一个设计友好 API 的人来说确实需要;另外接口太小,名称统一会导致被很多其他拥有相同字段接口所隐式的实现。

在编写项目中最常见的就是把基础标量类型封装为一个 struct 类型结构体,例如下面代码是一段实现了 gossip 协议所需要的代码,分别为 Node 和 Cluster 结构体,这两个结构体要提供外部的包进行调用和访问。这两个结构体的字段都是大写开头,这是由 Go 访问控制权限规则设计要求的,但是在某些需求下为了不让外部包的调用者能直接通过 Node {} 构造器来创建 Node 类型的实例,只能通过 gossip.NewNode() 进行实例化一个节点,因为 gossip.NewNode() 内部会提供一些默认的初始化规则。在这种场景下只能使用下面的方式来封装一个 innerNode 类型,将其嵌入到 Node 中,这样就可以避免其他人滥用 Node {} 创建,代码如下:

type (
    // 类似于私有化 Node {} 构造器,其他包就不能直接 {} 来创建 Node 实例
    // 只能通过 gossip.NewNode() 进行实例化一个节点
    innerNode struct {
        ID        string // 节点唯一标识
        Address   string // 节点地址
        Heartbeat int    // 心跳计数
        Timestamp int64  // 最新心跳时间
        Alive     bool   // 节点状态
        Hash      uint32 // 节点哈希值,用于一致性哈希
    }
    innerCluster struct {
        Neighbors map[string]*Node // 所有已知节点的信息
        Self      *Node            // 本节点信息
        Mutex     sync.Mutex       // 保护 Nodes 的并发访问
        Interval  time.Duration    // 心跳间隔
        Timeout   time.Duration    // 失效阈值
        HashRing  []*Node          // 一致性哈希存储范围的键
    }

    // Node 协议集群中的节点
    Node struct {
        innerNode
    }

    // Cluster 协议的集群集合
    Cluster struct {
        innerCluster
    }
)

// NewNode 只能使用此暴露外部的函数进行创建
func NewNode(id, addr string) *Node {
    node := &Node{
        innerNode{
            ID:        id,
            Address:   addr,
            Heartbeat: 0,
            Timestamp: time.Now().Unix(),
            Alive:     true,
        },
    }
    // 通过节点 ID 算出在哈希环中的值
    node.Hash = NodeHash(id)
    return node
}

在这段代码逻辑中存在一个问题,就是由于 struct 中的字段都是以小写开头,导致如果需要对此 Node {} 这个结构体进行 JSON 正反序列化,就会出现无法进行序列化的情况,原因就是因为上面的需求导致的,导致一个很矛盾的问题。要解决这个序列化问题,可以让要被序列化的 struct 实现自定义的 MarshalJSON 方法,这种方式要额外的编写正反序列化的代码实现。另外就是无法被反射编程。

func OpenFS(opt *Options) (*LogStructuredFS, error) {
    err := checkFileSystem(opt.Path)
    if err != nil {
        return nil, err
    }

    once.Do(func() {
        fsPerm = opt.FsPerm
        instance = &LogStructuredFS{
            indexs:    make([]*indexMap, indexShard),
            regions:   make(map[uint64]*os.File, 10),
            version:   1,
            directory: opt.Path,
        }

        for i := 0; i < indexShard; i++ {
            instance.indexs[i] = &indexMap{
                mux:   sync.RWMutex{},
                index: make(map[uint64]*INode),
            }
        }

        // 先对已有的数据文件执行恢复操作,并且初始化内存中的数据版本号
        err := instance.recoverRegions()
        if err != nil {
            return nil, fmt.Errorf("%w", err)
        }

    })

    // 单例子模式,但是挡不住其他包通过 new(LogStructuredFS) 也能创建一个实例,那这样根本不起作用了
    return instance, nil
}

错误处理和 defer 关键字的在关闭文件描述时发生干扰,在光读文件下直接执行 close 操作是没有问题的,例如下面的代码:

        file, err := os.Open(filePath)
        if err != nil {
            return fmt.Errorf("failed to open index file: %w", err)
        }
        defer file.Close()

上面这段只以只读的方式打开了文件,所以只需要执行 defer file.Close() 操作就可以完成文件句柄关闭,但是下面这种情况在写入情况进行关闭,因为操作系统内核有缓冲区的存在,不一定能及时刷盘,所以在关闭文件的时候还要执行 file.Sync() 操作保证内核缓冲区的数据能及时刷盘再关闭,例如下面的代码:

    err := lfs.active.Sync()
    if err != nil {
        return fmt.Errorf("failed to sync active file: %w", err)
    }

    err = lfs.active.Close()
    if err != nil {
        return fmt.Errorf("failed to close active file: %w", err)
    }

优化之后的代码:

    err := utils.CloseFile(lfs.active)
    if err != nil {
        return fmt.Errorf("failed to close active file: %w", err)
    }

defer 关键字是用来延迟执行某个函数用途,并且保证在函数执行栈出现异常或者 return 时,能正常执行某个预定函数操作。在读写文件关闭操作时经常使用到 defer 来关闭文件描述,这段代码目前的逻辑中是没有任何问题的,但在结合 defer 场景下就会出现问题,一遍为了防止文件句柄打开忘记关闭情况下,在打开文件操作的代码逻辑后面就会紧跟上 defer 的操作,但 defer 并不能处理这个函数的调用返回值...这就导致执行 defer utils.CloseFile(lfs.active) 还是会丢弃掉 CloseFile 函数所传递出来的关键的错误信息...

目前个人感觉 Go 优点只是能静态编译,将程序源代码直接通过编译,编译为 Native Code 直接在计算机上运行,有着较快的冷启动和低内存占用特性。静态编译在 Cloud Native 场景下确实合适,并不意味着它能和其他基于 VM 的语言相比,例如 JVM 可以在运行时动态反射编程,Go 虽然也有,但我觉得并没有能媲美 Java 一样反射能力,毕竟写业务代码 JVM 可以支持动态类的 AOP 和 DJ 编程语言才是最爽的。

另外有人可能看到这里要和我抬扛 Go 语言不需要类似于 JVM 这样的形式来运行程序,Java 和 Go 在设计的时候存在着本质上的区别,JVM 是语言平台,基于 JVM 去实现的编程语言,你只需要实现将源代码的 AST 转换为 JVM 字节码就行了,至于内存管理和垃圾回收工作全部由 JVM 来完成,这对于想要去实现一门编程语言的人来说只需要关注于 JVM 编译器前端,对应着 AST => ByteCode 的过程,无需关系其他的实现,标准库可以服用 JRE 里面的 API 来完成。

便宜 VPS vultr
最后修改:2025 年 04 月 10 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !