V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
Ansen
V2EX  ›  Go 编程语言

Go 中为啥喜欢这样创建变量

  •  
  •   Ansen · 2020-08-03 11:52:18 +08:00 · 7101 次点击
    这是一个创建于 1573 天前的主题,其中的信息可能已经有所发展或是发生改变。

    经常能看到类似 newRouter 这样的代码

    type router struct {
    	roots    map[string]*node
    	handlers map[string]HandleFunc
    }
    
    func newRouter() *router {
    	return &router{
    		roots:    make(map[string]*node),
    		handlers: make(map[string]HandlerFunc),
    	}
    }
    
    func (r *router) AddRoute(method string, pattern string, handler HandleFunc) 
    func (r *router) getRoute(method string, path string) (*node, map[string]string)
    

    然后在其它地方这样调用

    r := newRouter()
    r.AddRoute()
    

    为啥不直接这样呢?

    var r router
    

    代码都能看懂,但是不知道为啥喜欢这样用,或者这样用的好处是啥?

    我能想到的是:

    1. 通过指针来保证代码块中访问的一致性
    2. 避免参数复制产生的内存消耗

    还有其它的好处吗?

    在其它项目中还会看到这样的代码

    package gulu
     // ..........
    
    // Result represents a common-used result struct.
    type Result struct {
    	Code int         `json:"code"` // return code
    	Msg  string      `json:"msg"`  // message
    	Data interface{} `json:"data"` // data object
    }
    
    // NewResult creates a result with Code=0, Msg="", Data=nil.
    func (*GuluRet) NewResult() *Result {
    	return &Result{
    		Code: 0,
    		Msg:  "",
    		Data: nil,
    	}
    }
    
    package controller
    // .... 
    func addSymArticleAction(c *gin.Context) {
    	result := gulu.Ret.NewResult()
    	defer c.JSON( http.StatusOK, result)
    
    	arg := map[string]interface{}{}
    	if err := c.BindJSON(&arg); nil != err {
    		result.Code = util.CodeErr
    		result.Msg = "parses add article request failed"
    
    		return
    	}
        // ....
    }
    
    43 条回复    2020-08-04 00:39:28 +08:00
    Trim21
        1
    Trim21  
       2020-08-03 11:54:13 +08:00 via iPhone
    用来给属性赋默认值…
    ahmcsxcc
        2
    ahmcsxcc  
       2020-08-03 11:56:07 +08:00
    map 不赋值会 painc 啊
    coderxy
        3
    coderxy  
       2020-08-03 11:58:49 +08:00
    map 要初始化的,不然会报错
    janxin
        4
    janxin  
       2020-08-03 12:05:14 +08:00   ❤️ 1
    你举的例子应该没有这样实际运行过,上面说的 panic 问题主要是针对 map 之类的特殊类型

    另外一个一些必须进行初始化操作因为没有初始化方法的原因需要通过这种方式实现,比如默认值为非空。

    当然并不是所有的情况都是需要这样进行的,比如官方库提供的 sync.Mutex 也是直接使用的
    Jirajine
        5
    Jirajine  
       2020-08-03 12:05:57 +08:00 via Android   ❤️ 10
    这属于 go 的糟粕之一。
    go 号称所有类型创建时就会默认初始化,但类型只要包含引用这个默认的初始化值基本上就没法用(像这里 map 被初始化成 nil 而不是一个空的 map )。
    于是 go 在编译时不进行静态检查而是给你一个“脏的初始值”,你那样用编译时没问题但跑着跑着就炸了。
    BingoXuan
        6
    BingoXuan  
       2020-08-03 12:08:41 +08:00
    类似于构造函数,直接声明创建变量没问题,但是变量初始值怎么确定是问题。
    ysc3839
        7
    ysc3839  
       2020-08-03 12:21:55 +08:00 via Android
    个人理解:这里的 map 类似于 C++ 中的 unique_ptr,它可以自动 delete,但不能自动 new 。
    感觉根本问题是没有构造函数,只能用一个普通函数模拟构造函数。
    jybox
        8
    jybox  
       2020-08-03 12:32:47 +08:00   ❤️ 1
    简单来说就是代替构造函数
    huweic
        9
    huweic  
       2020-08-03 13:07:03 +08:00
    过度解读一下,除了 map 初始化的问题,同时 New 的好处是为了方便以后扩展维护,指定默认值,执行其他初始化操作等,先把口子留着
    zjsxwc
        10
    zjsxwc  
       2020-08-03 13:14:02 +08:00
    指针空值问题,
    总要有个初始化的构造函数吧
    wangritian
        11
    wangritian  
       2020-08-03 13:17:24 +08:00
    这是我不喜欢 go 的一个地方,没有构造函数,方法没有默认参数
    ss098
        12
    ss098  
       2020-08-03 13:51:49 +08:00
    Go 语言没有类,同样也没有构造器语言特性。

    NewType 是 Go 语言的构造器命名惯例,等同于 Python __init__,PHP __construct 。
    araraloren
        13
    araraloren  
       2020-08-03 14:02:28 +08:00
    所以为啥不弄一个 new 之类的构造函数。。
    go 开发团队:为了保持解析器简单 :D
    szzhiyang
        14
    szzhiyang  
       2020-08-03 14:09:25 +08:00
    @Jirajine 那你能给出更好的解决方案吗?
    virusdefender
        15
    virusdefender  
       2020-08-03 14:15:18 +08:00
    没法强制指定构造函数确实是我不太喜欢的一点
    hst001
        16
    hst001  
       2020-08-03 14:34:40 +08:00 via Android
    这是一种类似工程规范的东西,普通的 NewXxx,有些会有多个初始变体会写成类似 NewXxxWithAaa 或 NewXxxFromAaa 之类的可读形式,隐藏掉没必要暴露的细节,无论对于调用者还是开发者,都可以提高可维护性。
    zivn
        17
    zivn  
       2020-08-03 14:35:18 +08:00   ❤️ 1
    1. 对结构体所有的属性进行必要的初始化,避免 panic ;
    2. 方便做一些预处理,初始化函数可以传入参数,也可以返回错误,和 __init__ 、__construct 还不太一样;

    另外 map 和 slice 底层定义都是结构体,一堆属性和指针的结合,区别在于 slice 的 append 函数可以自动初始化和扩容底层的数组结构,而 map 是直接 key-value 赋值的,不初始化底层结构,所以用 var 创建的 slice 一般可以正常使用,而 map 不行,会 panic,这个不算脏的初始值,也算不上是糟粕;
    hakono
        18
    hakono  
       2020-08-03 14:37:40 +08:00   ❤️ 3
    上面虽然基本都提到了,但是用更简单易懂的方法来说明的话,那就是因为 Go 语言的类( struct )不存在构造函数这种东西,导致创建类的时候做不到普通语言那样 MyClass(para1, para2, para3,...) 。
    所以如果你想在创建类的时候初始化一些什么东西的时候,就只能用这种 NewXXXX() 的工厂函数来创建。总之这点我也用得很不爽,明明是面向对象,但用工厂函数写总给人一种自己在写面向过程一样
    vvmint233
        19
    vvmint233  
       2020-08-03 14:54:56 +08:00   ❤️ 1
    var r router 只是变量的声明而已, 对于初始化新的结构体我有时候会 new(router).Init("aa"), Init()用来返回原结构体指针, 这样可以链式. 而且已经有 new 这个函数了, 其他类似的关键字会显得的有些多余
    另外 gin*Context 的那个, 其实可以自己把 handler 封装一下, 把 if else 压缩在一个函数里面, handler 主体依旧可以符合带返回值函数的样子, 对于 gin 来说, 请求的处理是 steaming 的, 所以没有返回符合框架的设计
    CNife
        20
    CNife  
       2020-08-03 15:25:23 +08:00   ❤️ 1
    这是 Go 语言设计中比较差的地方。
    所有类型都有默认值,但这个默认值不一定有用,甚至可能都不是合法值。
    对于 slice,你 var s []int 是合法的,nil 和空 slice 是一回事,append 和 len 也能正确处理 nil 的情况,这时候体验很好。
    但对于 map,你 var m map[int]int 就是个非法值,不论是存是取都会炸,这时候 nil 和空 map 就不是一回事了。

    根本原因是,每个类型都有自己的初始化方式,它自己最清楚该怎么初始化自己,这就是构造函数的作用。类型可以不定义构造函数,这时候编译器才需要接管类型的初始化工作,给类型一个默认的零值。如果类型自己定义的初始化的方式,编译器就不要越俎代庖了。同时,构造函数是初始化对象的唯一、统一的方法,类型的作者可以由此控制新的对象必定处于合法的初始状态。
    但 Go 语言说,没有构造函数了,所有类型都有默认的零值。但有的类型,零值是合法的初始值,但其他类型就未必了。比如这个类型里有指向堆内存的指针,需要在用之前分配内存,nil 肯定不中用了;有的类型,
    不过,这也同时取决于类型的设计者有没有考虑到这点,有没有把零值也纳入到合法值的范畴。但这就比较麻烦了,难道要每个函数都要在开头判断一下传进来的是不是 nil,如果是再做个初始化?这可太蠢了,不如直接在文档里规定不允许用 var,必须用 NewXXX 新建对象。
    NCZkevin
        21
    NCZkevin  
       2020-08-03 15:30:59 +08:00
    go 开发团队喜欢简洁,导致 go 有很多特有的写法,适应了很久才习惯
    CNife
        22
    CNife  
       2020-08-03 15:31:33 +08:00
    @vvmint233 var name Type 不能叫声明,应该叫零值初始化,确实分配了内存也赋予了零值。
    neoblackcap
        23
    neoblackcap  
       2020-08-03 15:37:58 +08:00
    默认的构造函数没有什么用,C++里面要删除各种默认的构造函数的情况还少么?这个真的不是什么黑点,构造函数本质上跟现在这样的静态函数没有什么区别。
    KickAssTonight
        24
    KickAssTonight  
       2020-08-03 15:38:56 +08:00
    主要是因为没有构造方法吧
    Jirajine
        25
    Jirajine  
       2020-08-03 15:47:03 +08:00 via Android   ❤️ 1
    @szzhiyang 楼里都在说 constructor 的问题,这个不是 constructor 的问题,而是 go 自作聪明搞得所有变量都默认初始化。
    正确的做法是编译器静态检查访问所有对未初始化变量的访问并在编译时报错,而不是给个不正确的初始值然后运行时爆炸。
    这种默认初始化值还有一些其他问题,比如 int 传个 0 进来你不知道它是 uninitialized 还是就是 0 。
    icexin
        26
    icexin  
       2020-08-03 16:03:44 +08:00
    go 的变量初始化就是申请内存,然后 memset 为 0 。如果需要额外的初始化就用一个函数来进行初始化工作,思路很清晰。这一块倒是贯彻了 python 里面的 Explicit is better than implicit 。隐式的构造函数看着很方便,但有时候反而带来了无谓的开销。
    vvmint233
        27
    vvmint233  
       2020-08-03 16:35:38 +08:00
    @CNife 是我不严谨了, 因为 nil 我更习惯叫声明
    szzhiyang
        28
    szzhiyang  
       2020-08-03 16:42:25 +08:00
    @Jirajine 也许是因为检查各个变量是否被初始化会让编译期检查变得复杂且缓慢吧,Go 有不少设计就是为了确保编译流程简单且迅速。
    dog82
        29
    dog82  
       2020-08-03 16:42:44 +08:00
    我写了 2 年多 Go 了,还是对某些语法很排斥。
    特别是类型转换,有时会被气爆炸!
    uint32 不能隐式转成 uint64
    MiskoLee
        30
    MiskoLee  
       2020-08-03 16:44:17 +08:00
    不知道 LS 一段人在说什么。。。。。。。

    这个东西叫做 类型断言。。。。。。,很多现代静态语言都有这个功能,比如 Rust 。
    tairan2006
        31
    tairan2006  
       2020-08-03 17:19:50 +08:00
    就是初始化的问题,go 没构造函数其实影响不大
    janxin
        32
    janxin  
       2020-08-03 18:02:47 +08:00
    @Jirajine 这只是一个 null safety 问题,至于需要确认是否初始化有其他方式可以确定。

    在其他语言里也不是什么很特殊的东西。

    https://kotlinlang.org/docs/reference/null-safety.html
    https://dart.dev/null-safety/understanding-null-safety
    keepeye
        33
    keepeye  
       2020-08-03 18:08:25 +08:00
    默认值呗
    janxin
        34
    janxin  
       2020-08-03 18:10:10 +08:00
    关于 null safety,谷歌开发者有一篇文章关于这个问题( Dart ):

    https://mp.weixin.qq.com/s/rgVJn928fyGunNO5kDKnSA

    但是如果允许 nil 这个特殊类型之后,如果库出现了问题,在没有异常处理的 Go 语言里没处理好可能程序动不动就崩了...
    nuk
        35
    nuk  
       2020-08-03 18:18:53 +08:00   ❤️ 1
    你这里 struct 的 field 都导出了,要是有没导出的咋办?
    这个用法基本上和 typedef 一个未声明结构的指针差不多
    只能从函数返回,实际上无法实例化
    zxlzy
        36
    zxlzy  
       2020-08-03 19:17:19 +08:00
    这不就是构造函数吗
    reus
        37
    reus  
       2020-08-03 20:33:17 +08:00
    @Jirajine 连零值的概念都不懂,就别满嘴糟粕糟粕了。map 的零值就是 nil,为啥?如果我只是想声明一个 map 变量,在用现有的 map 赋值,那声明的时候就初始化一个空的干嘛?多余!
    petelin
        38
    petelin  
       2020-08-03 20:45:01 +08:00   ❤️ 1
    @Jirajine 这就是构造函数啊,难道你们构造东西的时候不写点逻辑进去?

    还有,把创建的地方收在一起后期改造也好改啊, 要不然自己去初始化,项目里边 100 多个地方都 create,你想改个默认值都不能改。

    唉。。。
    petelin
        39
    petelin  
       2020-08-03 20:46:15 +08:00   ❤️ 1
    真的忍不住吐槽, 没有基本代码素养的人太多了。。。一个现代的语言动不动被吐槽糟粕。 真正的糟粕是 js 这种历史包袱一大堆的垃圾好吗
    Jirajine
        40
    Jirajine  
       2020-08-03 20:51:21 +08:00 via Android
    @reus 你觉得创建出来就是 nil,别人也可以觉得创建出来就得能用,不能是 nil 。
    问题就在于它明明可以在编译时检查避免访问未初始化变量,却偏要自作聪明的默认给一个“零值”作为初始值,这个“零值”有时候是想要的有时候不是。
    访问 undefined 变量在脚本语言中是很普遍的一类问题,go 作为静态编译型语言完全可以编译时就避免这类问题却不这么做,所以说是糟粕之一。
    Jirajine
        41
    Jirajine  
       2020-08-03 20:54:23 +08:00 via Android
    @petelin 我没有说构造函数有问题啊,你到底看没看我写的什么。
    我说的是变量默认初始化的问题,换一个正常点的语言楼主这种做法编译时就会得到错误“uninitialized variable”,而 go 的做法使其能够通过编译并在运行时崩溃。
    scnace
        43
    scnace  
       2020-08-04 00:39:28 +08:00 via Android
    @scnace 噫 为啥 YouTube 的链接被吃了(
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2566 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 04:58 · PVG 12:58 · LAX 20:58 · JFK 23:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.