当前位置: 首页 > article >正文

golang编程思考(1)——作用域

资源

标准库中文文档: Go语言标准库文档中文版 | Go语言中文网 | Golang中文社区 | Golang中国

编程标准: The Go Programming Language Specification - The Go Programming Language

大佬社区: Newest Questions - Stack Overflow

起因

问题的起源是一个学弟在编码的时候遇到了这样一个报错:encoder.Encode err: gob: type elliptic.p256Curve has no exported fields、对应以下这部分内容。

这个方法是去对字段实现一个序列化,gob这个包一般是实现rpc功能的时候会去使用的,但是我一般实现远程通信,基本使用protobufthrift这两个框架去实现的。

首先,我们对错因进行了分析:发现是变量无法导出的问题,此时我们隐约感受到是因为变量无法调用的原因,因此我们查询了gob的源代码:

func Register(value any) {
    // Default to printed representation for unnamed types
    rt := reflect.TypeOf(value)
    name := rt.String()
  1. 这个value是一个泛型,所以说什么数据类型都可以接受。

  2. 然后这个value传到了reflect这个方法里面,但是reflect包有一个奇怪的属性,就是它只能去使用大写的变量,不能使用小写的私有变量,详情可见官方说明的下面部分

这里面讲的是“要修改反射对象,该值必须为可设置”,那我们就需要理解一下什么是可设置的。C语言中的可设置常见的就是使用指针去修改内存地址中的内容,golang包括java这些面向对象的语言,往往有或者这些概念去限制变量的作用域,因此我们看一下官方文档中对于“可导出”变量(也就是作用域)的说法

这里面就是说,如果我们想要在某个包外面访问某个变量,就需要让这个变量的首字母大写,这可能就是has no exported fields的原因说在,也就是说value收到的值如果是小写的,就可能会导致这个报错,因此我们去查看elliptic.P256()的代码

func P256() Curve {
       initonce.Do(initAll)
       return p256
   }

返回了一个p256,我们去看看它

var p256 = &p256Curve{nistCurve[*nistec.P256Point]{
       newPoint: nistec.NewP256Point,
   }}
​

破案了,这个返回值在声明的时候是小写的,也就是说它的作用域只限于这个包,它就是造成这个报错的最终原因。

相似问题出处:go - panic: gob: type elliptic.p256Curve has no exported fields - Stack Overflow

解决方案:(源代码属于thanks-for-not-adding-salt,以下是我的修改,如有需要可以访问原作者仓库)

package main
​
import (
    "bytes"
    "crypto/ecdsa"
    "crypto/elliptic"
    "encoding/gob"
    "fmt"
    "io/ioutil"
    "math/big"
    "sort"
)
​
// 负责对外,管理生成的钱包(公钥私钥)
//私钥1->公钥-》地址1
//私钥2->公钥-》地址2
//私钥3->公钥-》地址3
//私钥4->公钥-》地址4
​
type WalletManager struct {
    //定义一个map来管理所有的钱包
    //key:地址
    //value:wallet结构(公钥,私钥)
    Wallets map[string]*Wallet
}
​
// 新的序列化需要使用的组件 瞎起的名字
type SerializableWallet struct {
    PrivateKeyBytes []byte
    PublicKey       []byte
}
​
//条件判断:wm.loadFile()尝试调用加载钱包文件的方法,该方法会读取本地存储的钱包数据文件(如wallets.dat),并将其反序列化到Wallets结构体的map字段中。
//加载失败处理:如果加载失败(比如文件不存在或数据解码错误),loadFile()返回false,此时return nil表示无法成功初始化钱包集合,程序直接返回空值。
//文件操作:加载过程中会检查钱包文件是否存在,若存在则读取文件内容并进行gob解码;若文件不存在或读取失败,则可能触发错误处理逻辑(如直接返回或记录错误)。
func NewWalletManager() *WalletManager {
    wm := &WalletManager{
        Wallets: make(map[string]*Wallet),
    }
    if !wm.loadFile() {
        return nil
    }
    return wm
}
​
func (wm *WalletManager) createWallet() string {
    // 创建秘钥对
    w := newWalletKeyPair()
    if w == nil {
        fmt.Println("newWalletKeyPair 失败!")
        return ""
    }
​
    // 获取地址
    address := w.getAddress()
​
    //把地址和wallet写入map中: Wallets map[string]*wallet
    wm.Wallets[address] = w //<<<--------- 重要
​
    // 将秘钥对写入磁盘
    if !wm.saveFile() {
        return ""
    }
​
    // 返回给cli新地址
    return address
​
}
​
const walletFile = "wallet.dat"
​
func (wm *WalletManager) saveFile() bool {
    var buffer bytes.Buffer
    encoder := gob.NewEncoder(&buffer)
​
    // 创建一个可序列化的钱包映射
    serializableWallets := make(map[string]SerializableWallet)
    for address, wallet := range wm.Wallets {
        serializableWallets[address] = SerializableWallet{
            PrivateKeyBytes: wallet.PriKey.D.Bytes(),
            PublicKey:       wallet.PubKey,
        }
    }
​
    // 序列化可序列化的钱包映射
    if err := encoder.Encode(serializableWallets); err != nil {
        fmt.Println("encoder.Encode err:", err)
        return false
    }
​
    // 写入文件
    if err := ioutil.WriteFile(walletFile, buffer.Bytes(), 0600); err != nil {
        fmt.Println("ioutil.WriteFile err:", err)
        return false
    }
    return true
}
​
// 读取wallet.dat文件,加载wm中
func (wm *WalletManager) loadFile() bool {
    //判断文件是否存在
    if !isFileExist(walletFile) {
        fmt.Println("文件不存在,无需加载!")
        return true
    }
​
    content, err := ioutil.ReadFile(walletFile)
    if err != nil {
        fmt.Println("ioutil.ReadFile err:", err)
        return false
    }
    decoder := gob.NewDecoder(bytes.NewReader(content))
    var serializableWallets map[string]SerializableWallet
    if err := decoder.Decode(&serializableWallets); err != nil {
        fmt.Println("decoder.Decode err:", err)
        return false
    }
​
    for address, sw := range serializableWallets {
        privateKey := new(ecdsa.PrivateKey)
        privateKey.PublicKey.Curve = elliptic.P256()
        privateKey.D = new(big.Int).SetBytes(sw.PrivateKeyBytes)
        privateKey.PublicKey.X, privateKey.PublicKey.Y = elliptic.Unmarshal(privateKey.PublicKey.Curve, sw.PublicKey)
        wm.Wallets[address] = &Wallet{
            PriKey: privateKey,
            PubKey: sw.PublicKey,
        }
    }
​
    return true
}
​
func (wm *WalletManager) listAddresses() []string {
    var addresses []string
    for address := range wm.Wallets {
        addresses = append(addresses, address)
    }
​
    //排序, 升序
    sort.Strings(addresses)
    return addresses
}

作用域

借此机会,我们回顾一下golang中的引用、作用域以及其他部分的一些知识。

go因为使用结构体和泛型去实现面向对象,所以作用域这个很模糊。作用域(Scope)是指变量、函数、类型等标识符在程序中可以被访问的范围。Go 语言的作用域主要分为包级作用域(全局作用域)和局部作用域

值得注意的是,go的一个包中可以包含多个文件,这个和Java的类是不一样的。因此下面我们假设每个包中有多个并列的go文件,这些go文件的代码统称为在某个包中。

作用域分级

  1. 包级别变量(在程序中而且不是在函数,控制语句形成的代码块中):

    • 可以被包内的任何函数访问。

    • 如果变量或函数的名称以大写字母开头(如 GlobalVar 或 PrintMessage ),则它们是“导出的”(exported),可以被其他包通过包名访问(eg: service.UserPackcge)

    • 如果以小写字母开头(如 globalVar 或 printMessage ),则它们是“未导出的”(unexported),只能在定义它们的包内访问。

  2. 局部变量(也就是说只能在这个代码块中使用和存在):值得注意的是,局部变量可以与全局变量同名,但局部变量会“遮蔽”(shadow)全局变量

  3. 值得注意的是,go语言并非以物理的目录作为分界的方式,而是以包名。

    project/
    ├── main.go
    ├── pkg1/
    │   ├── a/
    │   │   └── a.go
    │   ├── b/
    │   │   └── b.go
    │   └── pkg1.go
    └── pkg2/
        ├── c.go
        └── d.go

    比如上方的目录,在pkg1.go文件中是无法调用a、b包中的私有变量的,主要原因是pkg1.go属于pkg包,这个包是和a、b包是并列的

项目组织管理

首先,当我们开发一个go语言程序时,应当对项目结构有如下三点考虑

  1. 打包时间:对于复杂的项目,我们应当避免过早的打包,可以先完成程序开发,在完成某个版本之后再进行结构重组

  2. 打包大小:避免简单的进行组装,因为这会让每个包的作用含糊不清;也不要太小的打包,这会使项目结构复杂

  3. 打包规范:

    /cmd  主源文件 比如foo应用程序的源文件应该在/cmd/foo/main.go
    /internal  不希望其他人为程序应用导入库的私有代码
    /pkg   公开的公共代码
    /test  额外的外部测试和测试数据
    /configs  配置文件
    /docs  设计文档和用户文档
    /examples  应用程序和公共库示例
    /api  API契约文件
    /web  特定的web应用程序的资产(静态文件等)
    /build  打包和持续集成的文件
    /scripts  用于分析和安装等的文件
    /vendor  应用程序依赖


http://www.kler.cn/a/571350.html

相关文章:

  • 【金融量化】Ptrade中如何获取各类回测数据?
  • 初始提示词(Prompting)
  • 009---基于Verilog HDL的单比特信号边沿检测
  • C语言文件操作学习笔记:从基础到实践
  • Spring WebFlux WebSocket 连接保持策略
  • 深入探索像ChatGPT这样的大语言模型-02-POST training supervised finetuning
  • ioday2----->标准io函数
  • python二级考试中会考到的第三方库
  • LeetCode 1745.分割回文串 IV:动态规划(用III或II能直接秒)
  • 对于基于RuleOS的DApp,如何进行安全的权限管理和访问控制?
  • 文本挖掘+情感分析+主题建模+K-Meas聚类+词频统计+词云(景区游客评论情感分析)
  • 项目升级golang版本
  • 初探WebAssembly
  • PyTorch 源码学习:GPU 内存管理之初步探索 expandable_segments
  • Hutool一个类型转换工具类 `Convert`,
  • 轻量服务器与普通云服务器区别是什么
  • Rust 面向对象特性解析:对象、封装与继承
  • 白帽子讲Web安全资源下载
  • 《Docker 核心概念揭秘:如何让软件开发像烹饪一样简单》
  • 【每日学点HarmonyOS Next知识】Web Header更新、状态变量嵌套问题、自定义弹窗、stack圆角、Flex换行问题