慎写指针类型的全局变量

简述:


在 关于range二三事[1] 第二个case中,介绍了对于指针类型的 切片/map变量A 的循环,要格外注意, 迭代出的value作用域是整个方法而非循环体内.

改进办法:在循环体中引入中间变量,"暂存"下每次迭代的value的值


但对于这个A,如果是全局变量,则又极有可能出现问题:

package main

import (
 "fmt"
)

type UserInfo struct {
 Name string
 Age  int
}

var (
 defaultInfo    = UserInfo{Name: "fliter", Age: 26}
 defaultInfoSli = []*UserInfo{&defaultInfo}
)

func main() {
 
 for _, v := range defaultInfoSli {
  tmp := v

  //go func() {
  tmp.Age = 100
  //}()
 }
 //time.Sleep(1e9)
 fmt.Println(defaultInfoSli[0].Age)
}

defaultInfoSli迭代出的v为指针类型,tmp仍为指针类型,对其赋值,会改变全局变量defaultInfoSli的值




复现:


在具体业务场景中,服务启动时初始化(取数据库或redis,或读取配置文件,加载到内存中)了一个全局变量.每个http请求过来,golang都会有一个新的协程去处理相关逻辑. 对于某个具体方法内的变量,对每次请求都是独立和隔离(每次请求都相当于一个个cellar,彼此之间不会有干涉和影响), 但对于永久存在内存中的全局变量,如果有对其写操作,每次请求都会影响该全局变量. 当出现并发请求如用户x和y同时请求接口, 两次请求都会改写全局变量, 这时就很可能出现返回的x和y的数据错乱


Demo如下:

package main

import (
 "encoding/json"
 "fmt"
 "github.com/davecgh/go-spew/spew"
 "log"
 "net/http"
)

type BookInfo struct {
 Title string
 Rank  int
 Data  interface{}
}

var (
 defaultBook1 = BookInfo{Title: "水浒传", Rank: 1}
 defaultBook2 = BookInfo{Title: "三国演义", Rank: 2}
 defaultBook3 = BookInfo{Title: "西游记", Rank: 3}
 defaultBook4 = BookInfo{Title: "红楼梦", Rank: 4}

 DefaultBookSli = []*BookInfo{&defaultBook1, &defaultBook2, &defaultBook3, &defaultBook4}
)

type CommonParams struct {
 ID   int64
 Name string
}

var (
 ModuleHandlers = map[int]func(params *CommonParams) *BookInfo{
  1: HandleTypeOne,
  2: HandleTypeTwo,
  3: HandleTypeThree,
  4: HandleTypeFour,
 }
)

func main() {

 fmt.Println(DefaultBookSli)

 http.HandleFunc("/index", deal) //设置访问的路由

 err := http.ListenAndServe(":80", nil) //设置监听的端口
 if err != nil {
  log.Fatal("ListenAndServe: ", err)
 }

}

func deal(w http.ResponseWriter, r *http.Request) {

 name := r.URL.Query().Get("name")

 //fmt.Println("name值为:", name)

 par := &CommonParams{
  ID:   0,
  Name: name,
 }

 // 获取相关数据
 for _, v := range DefaultBookSli {

  module := v

  //fmt.Println("module is:", module)
  //fmt.Println("排序为:", module.Rank)

  m := ModuleHandlers[module.Rank](par "module.Rank")

  // 填充模块数据
  if m.Data != nil {
   module.Data = m.Data
  }

  // (如果需要),重写模块标题(在此不需要)
  //if m.Title != "" {
  // module.Title = m.Title
  //}
 }

 //time.Sleep(1e9) //此处等待并不是因为协程,而是方便测试,不加这个等待,执行100次秒速就完成. 加这个等待是为了方便模拟"几个用户同时请求"
 //fmt.Println(DefaultBookSli[0].Rank)

 spew.Dump(DefaultBookSli)

 rsJson, _ := json.Marshal(DefaultBookSli)

 //fmt.Println(string(rsJson))    //这个写入到w的是输出到客户端的
 fmt.Fprintf(w, string(rsJson)) //这个写入到w的是输出到客户端的

}

func HandleTypeOne(p *CommonParams) *BookInfo {

 res := ""
 if p.Name == "施耐庵" {
  res = "我叫施耐庵,我是作者!"
 }
 return &BookInfo{Data: res}
}

func HandleTypeTwo(p *CommonParams) *BookInfo {

 res := ""
 if p.Name == "罗贯中" {
  res = "我叫罗贯中,我是作者!"
 }
 return &BookInfo{Data: res}
}

func HandleTypeThree(p *CommonParams) *BookInfo {

 res := ""
 if p.Name == "吴承恩" {

  res = "我叫吴承恩,我是作者!"
 }
 return &BookInfo{Data: res}
}

func HandleTypeFour(p *CommonParams) *BookInfo {

 res := ""
 if p.Name == "曹雪芹" {
  res = "我叫曹雪芹,我是作者!"
 }

 return &BookInfo{Data: res}
}

带着参数x, 使用Postman进行串行调用[2]100次,

5b3d4fe4d5e636b063596d0a6f3c3e3c.png

同时再访问这个接口,带参数y,此时可以发现,出现了数据错乱:

246984377c63faee268024f4a48cdabc.png


修改方案:


module := v这一步,实际上module依然是指针类型.

可以module := *v,这样module就不是指针类型,也就不会出现如上问题.


当时问题紧急,直接在里面新加了一个临时变量,即:

// 获取相关数据
 for _, v := range DefaultBookSli {

  module := v

  var temModule = &BookInfo{
   Title: module.Title,
   Rank:  module.Rank,
  }
  
  m := ModuleHandlers[temModule.Rank](par "temModule.Rank")

  // 填充模块数据
  if m.Data != nil {
   module.Data = m.Data
  }
 }



详细过程参见 私有笔记 并发写全局变量导致的数据错乱问题[3],印象深刻的一次体验

参考资料

[1]

关于range二三事: https://dashen.tech/2018/10/03/%E5%85%B3%E4%BA%8Erange%E4%BA%8C%E4%B8%89%E4%BA%8B/

[2]

Postman进行串行调用: https://www.cnblogs.com/stm32stm32/p/10434399.html

[3]

并发写全局变量导致的数据错乱问题: https://note.youdao.com/web/#/file/WEB058d4e136c3ee281320806fd45e3b07a/note/WEB6c29aebf58ba0868bdeef60f7e6bac40/

想要了解Go更多内容,欢迎扫描下方👇关注公众号,回复关键词 [实战群]  ,就有机会进群和我们进行交流


cc7305df6c0352cc352614554e5c4aa6.png

分享、在看与点赞Go 3fb1b3aa76aa033b57268bf92e9b69f7.gif

<think>嗯,用户问的是子程序如何调用主程序的变量。这个问题看起来是关于编程的,可能涉及到变量作用域或者程序结构。我需要先确定用户使用的编程语言,因为不同的语言处理方式不同。不过用户没有具体说明,所以可能需要给出一般性的解释,或者举几个常见语言的例子。 首先,子程序调用主程序的变量,通常涉及到变量的作用域问题。在大多数编程语言中,主程序中定义的变量如果在全局作用域,子程序可以直接访问。如果变量是局部的,可能需要通过参数传递或者使用全局变量声明。 比如在Python中,如果主程序里定义了一个全局变量,子函数可以通过global关键字来引用。不过通常推荐的做法是通过参数传递,这样代码更清晰,耦合度更低。例如: 主程序里定义变量x=5,然后在子函数中接收参数,处理后再返回结果。这样主程序调用子函数时传入x,子函数就能使用这个变量的值了。 但用户可能的情况是,他们可能没有使用参数传递,而想直接在子程序中访问主程序的变量。这时候需要解释全局变量的概念,同时提醒潜在的问题,比如变量被意外修改,导致程序难以维护。 另外,像C语言中,全局变量在函数外部定义,子函数可以直接使用。而局部变量则需要通过指针或者返回值来传递。例如,主函数中的局部变量如果要在子函数中使用,必须通过传地址的方式,或者将其声明为static,但static的话作用域还是限制在函数内,可能不行。 还有可能用户混淆了主程序和子程序的结构,比如在某个特定框架或环境中,主程序如何共享变量给子程序。这时候可能需要更具体的例子,比如在批处理脚本中,变量继承的问题,或者JavaScript中的闭包概念。 不过为了保持回答的通用性,我应该先解释一般情况,再分语言举例。比如分Python、C、Java等常见语言来说明不同的方法。同时强调最佳实践,比如尽量避免使用全局变量,而是通过参数传递,这样提高代码的可维护性和减少副作用。 另外,用户可能没有明确说明是哪种调用方式,是函数、子例程还是其他模块?可能需要涵盖不同的子程序类型,比如函数、方法、过程等。 总结下来,回答的结构应该是先解释作用域和变量传递的基本概念,然后分语言说明不同的实现方式,最后给出建议的最佳实践。同时注意提醒潜在的问题,比如全局变量的风险,参数传递的优点等。 现在需要检查是否有遗漏的情况,比如静态变量、闭包、或者面向对象中的类变量等。例如在Java中,类的静态变量可以被所有方法访问。或者实例变量在对象的方法中被共享。这可能也是用户想了解的情况之一,但用户的问题可能更偏向于过程式编程中的主程序和子程序。 需要确保回答准确,同时语言简洁易懂,适合不同层次的用户理解。可能用户是初学者,所以避免使用过于专业的术语,或者在使用时加以解释。</think>在编程中,子程序(如函数、方法)调用主程序的变量主要涉及**变量作用域**和**参数传递**两种方式。以下是具体实现方法和注意事项: --- ### 一、全局变量直接调用(不推荐) #### 适用场景 - 少量全局共享的配置或常量 - 临时调试代码 #### 示例(Python) ```python # 主程序中定义全局变量 global_var = 10 def sub_program(): # 子程序直接访问全局变量 print(global_var) # 输出: 10 sub_program() ``` #### 缺点 - 代码耦合度高,难以维护 - 可能被意外修改(可通过`global`关键字限制修改) --- ### 二、参数传递(推荐) #### 适用场景 - 大多数生产代码 - 需要明确输入输出的场景 #### 示例(Python) ```python def sub_program(input_var): return input_var * 2 # 处理主程序传递的变量 # 主程序调用 main_var = 5 result = sub_program(main_var) # 将变量传递给子程序 print(result) # 输出: 10 ``` #### 优点 - 明确输入输出,降低耦合 - 便于单元测试和调试 --- ### 三、面向对象编程中的类变量 #### 适用场景 - 多个方法需要共享同一变量 - 封装相关操作 #### 示例(Python) ```python class MyClass: def __init__(self): self.class_var = 20 # 类实例变量 def sub_method(self): print(self.class_var) # 通过self访问类变量 # 主程序 obj = MyClass() obj.sub_method() # 输出: 20 ``` --- ### 四、不同语言的特殊处理 1. **C语言**:通过指针传递地址 ```c void sub_program(int *var) { printf("%d", *var); // 通过指针访问主程序变量 } int main() { int main_var = 30; sub_program(&main_var); // 传递变量地址 return 0; } ``` 2. **JavaScript**:闭包捕获变量 ```javascript function main() { let mainVar = 40; function subProgram() { console.log(mainVar); // 闭包捕获外部变量 } subProgram(); // 输出: 40 } main(); ``` --- ### 五、最佳实践 1. **优先参数传递**:明确数据流,减少副作用 2. **全局变量**:易引发隐蔽的BUG 3. **封装成类**:通过面向对象管理共享状态 4. **常量使用全大**:如`MAX_SIZE`,明确不可变性 通过合理选择变量传递方式,可以编出更健壮、易维护的代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值