Golang中连接作用域变量的最佳实践是什么?

Golang中连接作用域变量的最佳实践是什么? 这确实暴露了我对Go语言的无知,因为对大多数人来说这肯定是个非常基础的问题——我刚刚开始学习。

在PHP中,当有人连接时,全局变量并不是真正的全局变量,因为它自动被限制在会话/连接范围内。一旦请求处理完成,这种"类似"全局变量就会自动销毁。同时,其他同时建立的连接完全隔离,因此可以存在多个独立的全局变量实例。就像任何编程语言一样,在PHP中优秀的程序员很少使用全局变量,但它们确实有其用途。特定连接的用户变量就是这样一个用例,由于它被广泛使用,显式传递给所有使用它的例程并不合理。

如果我正确理解了Go语言,全局变量是真正的全局变量,即使在连接之间,所有连接都能看到同一个全局变量。这意味着你不能简单地设置全局用户变量,因为下一个连接可能将其设置为不同的值,从而产生冲突。此外,它不会在连接结束时自动销毁,需要显式处理。

所以我的问题有两个方面:

  • 我对这些差异的评估有多接近?我在理解上遗漏了什么?
  • 对于"每个连接"的变量(比如与该连接关联的用户),最佳实践是什么?

更多关于Golang中连接作用域变量的最佳实践是什么?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

13 回复

你可以使用指针来实现!

更多关于Golang中连接作用域变量的最佳实践是什么?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


只要你在 http.HandleFunc() 或其他类似函数内部声明和使用变量,它就是安全且单用户的变量。

将整个 PHP 脚本想象成 http.HandleFunc(){/* 整个 PHP 脚本 */} 中的代码块。

所以在 Golang 中没有任何问题!

@AnikHasibul:非常出色——感谢您提供的精确说明。这让我明白,就"每个用户"而言,实际上无法将这样的变量作为例程可以像全局变量一样获取的东西,它始终必须明确作为需要该变量的例程的传递参数。

在我看来这没有问题,只是与我习惯的方式不同,因此我会相应地调整我的思路。

正如Norbet和Anik所提到的,Go语言中程序的结构与PHP有所不同。在Go中,您将在主函数中创建HTTP服务器,而处理程序将在您习惯编写的区域处理代码。这些处理程序会在每个请求时被调用,并符合无状态HTTP的特性。正如Lutz所说,如果您希望在HTTP请求之间保持数据持久性,您需要实现某种形式的会话状态来跟踪用户数据。

我认为即使在PHP中也不存在"连接"的概念。虽然HTTP在技术上使用了连接,但每个请求/响应对都是独立存在的。会话中可以存储数据,但每次呈现会话ID的请求都必须重新获取这些数据。

您是在询问会话机制吗?Go HTTP框架支持会话功能。或者您是在询问对全局服务器状态所做的修改?

// 代码示例保留原样

你能详细说明一下吗?你的意思是传递指针而不是变量?在这种情况下,你仍然在传递某些东西,因此函数对该传递变量仍然有定义,所以我不明白这如何有助于避免传递一个非常常见的变量(尽管指针本身具有其优势)。

话虽如此,PHP中的Global命令本质上是在设置一个指针(只是不在函数定义中),所以也许我该停止追问这个问题了。

是的,连接或会话,我并不想过于技术化——我想传达的是会话级变量的概念。据我所知,PHP中的全局变量并不像Go语言中那样存在,如果要区分的话,我会将PHP全局变量称为会话级变量,而将Go全局变量称为应用级变量。

既然我看到下面也有人提到了HTTP框架,那看起来就是答案了。谢谢。

PHP中的全局变量与Go语言中的全局变量具有相同的全局性。区别在于PHP程序的生命周期仅限于单个请求,并且会为后续请求反复重新启动,这就是为什么全局变量感觉像是仅绑定到单个请求的原因。

另一方面,Go程序的生命周期会跨越多个请求,因此全局变量也是如此。

我建议传递一个包含你所需值的结构体。在添加每个字段时都要仔细考虑,因为过于庞大的结构体与全局变量一样存在问题。

我知道Go语言没有任何问题,我认为它是一门很棒的编程语言,这也是我正在用它构建应用程序的原因。不过,可能因为我长期专注于PHP开发,所以需要花些时间来适应一些差异。

那么,假设有这样一个例子:

http.HandleFunc(){/* whole php script */}

如果我在这个函数的顶部声明一个变量,它会基于"每个请求"独立存在吗?例如,在第一个请求中我设置$User=1(请忽略语法问题,这里只是理论探讨),第二个请求中设置$User=2,这两个值会不会互相覆盖?还是说我的理解仍然存在偏差?

不,并非如此。是的,PHP 会为每个请求/会话/连接(无论您如何称呼)启动和终止进程,但变量是真正隔离的。例如,如果两个人同时连接,并且我设置了一个全局变量,这些在内存中是完全独立的实例。我认为在 Go 语言中的工作方式并不相同。以下链接可能比我更能清晰地说明 PHP 中的情况: https://stackoverflow.com/questions/36021571/php-global-variable-available-to-application-and-other-users

var ProgramBasisGlobal ="this will exist on every request"

http.HandleFunc(w http.ResponseWriter, r *http.Request) {
   ProgramBasisGlobal = "changed!" // 这将在程序的每个未来请求中被更改!

// 以下变量将在每个请求中声明和销毁!

    var userBasisVariable = "hello user" // 用户基础意味着每个请求基础
   var  AnotherUserBasisGlobalVariable ="Hello user this is global"


     showMyUsername := func() string{
            username := "Anik" // 所谓的局部/私有变量
           return username
         }()


       //showMyUsername 也是一个基于请求的全局变量
}

Reg:

如果两个人同时连接,我设置了一个全局变量,这些在内存中是完全独立的实例。

是的,因为两个用户都会获得各自独立的PHP"程序"实例。正如我所说,PHP的全局变量与Go中的全局变量一样具有全局性,区别在于程序的生命周期。

如果你使用Go进行CGI编程,全局变量的行为就会和PHP中一样,因为每个请求都会启动一个独立的Go程序实例。

Reg:

是的,连接或会话,我不打算说得太技术性

但你应该使用正确的术语,这样我们才能准确理解彼此的意思。

连接和会话之间存在很大差异。

在HTTP的情况下,我将连接理解为单个请求,而会话则由同一客户端发出的多个请求组成。

PHP全局变量无法在请求之间保持,因此不存在会话级别的全局变量。不过PHP有一个魔术变量,它在每个请求时都会从配置的会话存储中重新创建。

在Go语言中,你对全局变量作用域的理解基本正确。Go的全局变量确实是进程级别的,所有goroutine共享同一内存空间,这与PHP的请求隔离模型有本质区别。以下是详细解答:

1. 关于差异评估的准确性

你的评估非常准确:

  • Go的全局变量是真正的进程全局,所有goroutine共享
  • 没有自动的连接作用域隔离
  • 需要显式管理变量生命周期

2. 每个连接变量的最佳实践

方法一:使用context.Context(推荐)

package main

import (
    "context"
    "net/http"
    "time"
)

type userKey struct{}

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 为每个请求创建用户上下文
        user := &User{ID: "123", Name: "john"}
        ctx := context.WithValue(r.Context(), userKey{}, user)
        
        // 使用新的上下文继续处理
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func userHandler(w http.ResponseWriter, r *http.Request) {
    // 从上下文中获取用户信息
    if user, ok := r.Context().Value(userKey{}).(*User); ok {
        w.Write([]byte("Hello, " + user.Name))
    }
}

type User struct {
    ID   string
    Name string
}

方法二:请求级别的结构体封装

package main

import (
    "net/http"
    "sync"
)

type RequestContext struct {
    User *User
    // 其他请求特定数据
    mu sync.RWMutex
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := &RequestContext{
        User: &User{ID: "123", Name: "alice"},
    }
    
    processRequest(ctx, w, r)
}

func processRequest(ctx *RequestContext, w http.ResponseWriter, r *http.Request) {
    ctx.mu.RLock()
    defer ctx.mu.RUnlock()
    
    w.Write([]byte("User: " + ctx.User.Name))
}

方法三:使用goroutine本地存储(谨慎使用)

package main

import (
    "context"
    "net/http"
)

var userContexts = make(map[int64]*User)
var mu sync.RWMutex

func setUserForGoroutine(user *User) {
    mu.Lock()
    defer mu.Unlock()
    userContexts[getGoroutineID()] = user
}

func getUserFromGoroutine() *User {
    mu.RLock()
    defer mu.RUnlock()
    return userContexts[getGoroutineID()]
}

// 注意:需要实现getGoroutineID,但这种方法不推荐用于生产环境

方法四:依赖注入模式

package main

import (
    "net/http"
)

type Handler struct {
    UserService UserService
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    user := h.UserService.GetUserFromRequest(r)
    // 使用user处理业务逻辑
    w.Write([]byte("User: " + user.Name))
}

type UserService interface {
    GetUserFromRequest(r *http.Request) *User
}

type userService struct {
    // 可能的依赖项
}

func (s *userService) GetUserFromRequest(r *http.Request) *User {
    // 从请求中提取用户信息
    return &User{ID: "123", Name: "bob"}
}

关键要点

  1. context.Context 是处理请求范围数据的标准方式
  2. 避免使用真正的全局变量存储连接特定数据
  3. 每个请求应该有自己的数据实例
  4. Go的垃圾回收会自动清理不再使用的对象,无需手动销毁

在Web开发中,最常用的模式是通过中间件将连接特定数据注入到context中,然后在处理链中传递这个context。

回到顶部