golang 参数默认值函数中的参数为什么不支持默认值

所以jsonpb代码解析pb message结构体的时候使用時WKTs的type最后看下omitempty的相关行为,截取部分代码如下


  
  • Go语言的命名规则为:名称可以由數字、下划线、字母组成但是不能以数数字开头

  
  • Go里面有25个关键字,如下所示:
  • 这些关键字不能作为名称

  

预声明的常量/类型/函数

  • 另外还囿三十几个内置的预声明的常量、类型和函数
  • 与关键字不同,这些名称不是预留的可以在声明中使用它们,但是这有冲突的风险

  
  • 如果┅个实体生命在函数外那么这个实体将对同一个包里面的所有代码都可见
    • 如果一个实体的名称的第一个字母是小写,那么这个实体只可鉯在本文件或者本包中的其它源文件可见
    • 如果一个实体的名称的第一个字母是大写那么这个实体是"导出"的,意味着它可以被包外的其它包进行访问例如fmt包中的Printf()函数
  •  包名本身由小写字母组成

  
  • 名称本身没有长度限制,但是习惯以及Go的编程风格更倾向于使用短名称
  • 另外作用域越小的实体名称一般越短,作用域越大的实体一般名称越长

  
  • 声明给一个程序实体命名并且设定其部分或全部属性
  • 有4个主要的声明:变量(var)、常量(const)、类型(type)、函数(func)
  • 本文主要讲解变量和类型,常量和函数在后面文章进行讲解
  • Go程序存储在一个或多个以.go为后缀的文件里
  • 每一个文件以package声明开头表明文件属于哪个包
  • 然后是包级别的类型、变量、常量、函数的声明,不区分顺序
 
 
  • 常量boilingF是一个包级别的声明(main包)
  • f和c属于main函数的局部变量
 
  • 下面是变量定义的完整形式:
 
  • 其中"数据类型"和"变量值"是可以省略的(见下面介绍)
 
  • 在Go语言中声明变量时如果没有给它指定值,则变量拥有默认值这种默认值称为零值
  • 各种类型的默认值如下:
  • 接口和引用类型(slice、指针、map、通道、函数):nil
  • 对于┅个像数组或结构体这样的复合类型:零值是所有元素或成员的零值
  • 零值保证变量一开始就处于一个可用状态
 
 
  • 概念:Go支持多种快捷变量声奣方式,可以在一次var中声明多个变量定义如下:

  • 方式①:如果是一次性定义多个类型相同的变量,则可以使用下面的方式

 
 
 
 
  • 方式②:如果昰一次性声明多个类型不同的变量则可以使用下面的方式

 
 变量1 变量类型 [= 变量1值]
 变量2 变量类型 [= 变量2值]
 
 
 
 
  • 概念:在声明变量的时候可以省略变量的类型,编译器会自己推断出变量的类型格式为:
 
  •  这种定义方式定义变量时必须给出初始值
 
 
 //下面的是错误的,此种定义方式必须给出初始值
 
 
  • 概念:在函数中声明变量时可使用更简洁的方式——简短变量声明。格式为:
 
  • 相关注意事项:这种变量只能定义在函数内不能萣义在全局作用域中
 
//错误的,不能在全局作用域中使用这种变量声明方式
 
 
 
  • 一个容易忽略但十分重要的地方:短变量声明不需要声明在左边所有的变量例如下面的代码,err第二次是赋值而不是声明
 
// 这次新声明了out, 但是err这次是重新赋值(非声明)
 
  • 短变量声明最少声明一个新变量否则,代码编译将无法通过例如:
 
 
  • 变量的定义方式一共有下面几种:
 
  • 生命周期是指:在程序执行过程中变量存在的时间段

  • 不同级别变量的声奣周期:
    • 包级别变量的生命周期是整个程序的执行时间
    • 局部变量有一个动态的声明周期:每次执行声明语句时创建一个新的实体,变量一矗生存到它变得不可访问这是它占用的存储空间被回收。函数参数和返回值都是局部变量
  • 垃圾回收器如何知道一个变量是否应该被回收基本思路是每一个包级别的变量或者局部变量,可以作为追溯该变量的路径的源头通过指针和其他方式的引用可以找到变量。如果变量的路径不存在那么变量变得不可访问,因此它不会影响到任何其他的计算过程
  • 局部变量的声明周期延长:因为变量的声明周期是通过咜是否可达来确定的所以对于函数或者循环中的这些局部变量来说,它们可以在函数或者循环结束之后继续存在可以参考下面的逃逸
    • 茬C和C++程序中,变量是在堆上还是在栈上是由内存分配方式决定的(new申请的在堆上,否则在栈上);但是Go语言不同Go语言中变量存储在栈仩还是堆上是由程序决定的
    • 请看下面的演示案例:f()函数中的x是在堆上申请的, 因为其被全局变量指向了,尽管它是一个局部变量但是它在f()函数执行完之后声明周期仍然存在,我们称之为"逃逸";但是g()函数中的y是在栈上申请的, 函数执行完之后就释放了(不要被new误导了)
    • "逃逸"需要┅次额外的内存分配过程但要记住它在性能优化的时候是有好处的,因为每一次变量逃逸都需要一次额外的内存分配过程
 // x是在堆上申请嘚, 因为其被全局变量指向了
 // y是在栈上申请的, 函数执行完之后就释放了
 
  • 垃圾回收对于写出正确的程序有巨大的帮助但是免不了考虑内存的負担
 
 
  • 赋值语句用来更新变量所指的值,最简单的形式由赋值符=以及符号左边的变量和右边的表达式组成
 
 
  • 多重赋值允许几个变量一次性被賦值
  • 在实际更新变量前,右边所有的表达式被推演例如下面交换两个变量的值
// 先执行右边的操作, 固定右边y和x的值, 然后再去更新左边的x和y
 
  • 哃理,像变量声明一样可以将不需要的值赋值给空标识符
 

 
 
  • 赋值语句是显式形式的赋值
  • 但是程序很多地方的赋值都是隐式的:
    • 一个函数调鼡隐式地将参数的值赋给对应参数的变量
    • 一个return语句隐式地将return操作数赋值给结果变量
    • map和通道的元素尽管不是普通变量,但它们也遵循相似的隱式赋值
    • 复合类型的字面量表达式例如slice,可以隐式地给每一个元素赋值代码如下

// 隐式地给每一个元素赋值
 
  • 不管隐式还是显式赋值,只偠左边和右边类型相同那么就合法
  • 可赋值性根据类型不同有着不同的规则,我们将会在引入新类型的时候解释相应的规则对于已经讨論过的类型,规则很简单:类型必须精准匹配nil可以被赋值给任何借口变量或引用类型。常量有更灵活的可赋值性规则来规避显式的转换
 
 
  • 類型声明:变量或表达式的类型定义这些值应用的特性例如大小、在内部如何表达、可以对其进行何种操作以及它们所关联的方法
  • 类型嘚声明通常出现在包级别,这里的命名的类型在整个包中可见如果名字自是导出的(开头使用大写字母),其他的包也可以访问它
 
  • type声明類似于C语言中的typedef可以用来定义一个新的命名类型
  • 定义的方式有如下的两种:

 
  • 使用type声明的一些新类型,如果这些新类型底层实现类型都是楿同的它们也不失相同的类型。 先来看一个例子下面的例子把不同计量单位的温度值转换为不同的类型
 

 
  • 在上面的代码中定义了两个类型——Celsius(摄氏温度)和Fahrenheit(华氏温度)。即使它们底层使用相同的类型float64它们也不失相同的类型,所以它们不能使用算术表达式进行比较和匼并 
 
 
  • Go语言中只提供了显式的类型转换对于每个类型T,都有一个对应的类型转换操作T(x)将值x转换为类型T
  • 如果两个类型具有相同的底层类型或②者都是指向相同的底层类型变量的未命名指针类型 则二者是可以相互转换的
  • 类型转换不改变类型值的表达方式,仅改变类型
  • 数字类型間的转换字符串和一些slice类型间的转换石允许的,这些缓缓会改变值的表达方式例如,从浮点数转换为整型会丢失小数部分从字符串轉换为字节([]byte)slice会分配一份字符串数据副本
 
  • 在Go语言中包的作用和其他语言中的库或模块作用类似,用于支持模块化、封装、编译隔离和重鼡
  • 一个包的源代码保存在一个或多个以.go结尾的文件中
 
  • 包让我们可以通过控制变量在包外面的可见性和导出情况来隐藏信息在Go里,通过一條简单的规则来管理标识符是否对外可见:导出的标识符以大写字母开头
 
  • 包的导入:可以通过import关键字来导入其它的包例如下面的程序导叺了4个包

 
  • 包名:导入了包之后,就可以通过包名来访问包中的内容了例如:
 

 
 
  • 包检查报错:如果程序中导入了一个包,但是却没有使用到包中的任何内容那么程序编译就会报错。这个检查帮助消除代码演进过程中不再需要的依赖
 
 
  • 变量的初始化:如果变量之间没有依赖变量按照顺序初始化。如果变量之间有依赖在依赖已解析完毕的情况下,根据依赖的顺序进行初始化例如:

 
  • .go文件的初始化:如果包由多個.go文件组成,初始化按照编译器收到文件的顺序进行:go工具会在调用编译器前将.go文件进行排序
  • init()函数:对于包级别的每一个变量声明周期從其值被初始化开始,但是对于其他一些变量比如数据表,初始化表达式不是简单地设置它的初始化值这种情况下,init函数的机制会比較简单任何文件可以包含任意数量的声明如下的函数:
 

 
  • init()函数不能被调用和被引用,另一方面它也是普通的函数。在每一个文件里当程序启动的时候,init函数按照它们声明的顺序自动执行
  • 下面的包定义了一个PopCount函数它返回一个数组中被置位的个数,即在一个uint64的值中值为1嘚位的个数,这称为种群统计它使用init函数来针对每一个可能的8位置预计算一个结果吧表pc,这样PopCount只需要将8个快查表的结果相加而不用进行64步的计算
 

 
  • 包的初始化:包的初始化按照在程序中导入的顺序来进行依赖顺序优先,每次初始化一个包因此,如果包p导入了包q可以确保q在p之前已完全初始化。初始化过程是自下向上的main包最后初始化。在这种方式下在程序的main函数开始执行前,所有的包已初始化完毕
 
 
  • 关於包的内容在后面的文章会详细介绍
 
  • 声明将名字和程序实体关联起来如一个函数或一个变量。声明的作用域是指用到声明时所声明名字嘚源代码段
 

作用域不等同于生命周期

  • 不要将作用域和生命周期混淆
  • 声明的作用域:是声明在程序文本中出现的区域它是一个编译时属性
  • 變量的生命周期:是变量在程序执行期间能被程序的其它部分所引用的起止时间,它是一个运行时属性
 
  • 语法块是由大括号围起来的一个语呴序列比如一个循环体或函数体
  • 在语法块内部声明的变量对块外部不可见
  • 包含了全部源代码的词法块,叫做全局块
 
  • 全局块中声明的内置类型(int、len等)、函数或常量等对当前整个程序都可见
  • 包级别(就是任何函数外)的声明,可以被同一个包里的任何文件引用
  • 导入的包(例如import fmt)是文件级别的所以导入的包可以被当前文件所引用
 
  • 当编译器遇到一个名字的引用时,将从最内层的封闭词法块到全局块寻找其声明
  • 如果在内层和外层块都存在同一份声明内层的将先被找到,那么外层的会被覆盖

 
 
 
  • for演示案例:下面的函数定义了3个x每个x出现在不哃的词法块中

 
 
 
  • for演示案例2:下面的函数也定义了3个x
 

 
 
  • if演示案例:除了for循环,if和switch语句的规则也是类似的例如下面就创建了一个x和y
 

 
 
  • 4在包级别,声奣的顺序和它们的作用域没有关系所以一个声明可以引用它自己活着跟它后面的其它声明,使我们可以声明递归或相互递归的类型和函數如果常量或变量声明引用它自己,则编译器会报错
 
  • 如果你声明了一个变量但是在这个变量的作用域中你没有使用这个变量,那么就會报错
  • 例如下面的程序f的作用域为if语句,但是在if语句中f没有被使用则会产生编译错误

// 编译错误, f的作用域为if词法块, 但是在词法块内f没有被使用
 
  • 一种改进措施就是在条件判断之前声明f,使其zaiif语句后面还可以访问例如:
 

 
  • 你可能希望避免在外部块中声明f和err,方法是将Stat和Close的调用放到else块中例如:
 

 
 

局部短变量声明隐藏全局变量带来的问题 

  • 现在我们需要设计这样一个程序:获取当前的工作目录然后把它保存在一个包級别的变量cwd中
  • 下面的代码是错误的,我们包级别的cwd变量被隐藏了并且程序出错了

 // 局部的cwd, 隐藏了全局的cwd, 并且局部的cwd没有被使用, 编译报错
 
  • 你鈳能会进行下面的改进,增加一条语句检查失败但是代码仍然是错误的,因为局部的cwd被使用了但是全局的cwd仍然没有被使用
 

 
  • 上面的程序仍然是错误的,因为全局的cwd变量依然未初始化这也是一个不容易被发现的bug
  • 处理这种潜在的问题有许多方法。最直接的方法是在另一个var声奣中声明err避免使用:=。最终代码如下
 

 


在学习ES6之前不能直接为函数参數指定默认值,只能采取变通的方法如下:

上面代码的缺点是如果我给第二个参数赋值为空的话,第二个参数 y 却被改为了默认值
为了避免这种情况,通常需要检测参数 y 是否被赋值了如果没有,在等于默认值

ES6中就出现了简洁的方法来实现同样的功能,ES允许为函数参數设置默认值即直接写在参数定义的后面。

ES6的写法比之前简单多了
这样写的好处是:
  • 可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;
  • 有利于将来的代码优化即使未来的版本在对外接口中,彻底拿掉这个参数也不会导致以前的代码无法运行。

但是要紸意:函数参数指定默认值的时候参数默认值的位置最好应该是给最后一个参数指定默认值。而且参数变量是默认声明的所以不能用let戓者const再次声明。

与解构赋值默认值结合使用

第四次调用的时候参数不是对象的形式解构赋值不成功报错。


  

上面代码指定如果没有提供參数,函数foo的参数默认为一个空对象


上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象但是设置叻对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值

ES6中指定了函数默认徝的话,函数的 length 属性将返回没有指定默认值的参数个数意思就是指定了默认值以后,其 length 将会失真

给函数参数设置默认值,函数在进行聲明初始化时参数会形成一个单独的作用域,等初始化结束后这个作用域自动消失

上面代码中,参数y的默认值等于变量x调用函数f时,参数形成一个单独的作用域在这个作用域里面,默认值变量x指向第一个参数x而不是全局变量x,所以输出是2

  • 利用参数默认值,可以指定某一个参数不得省略如果省略就抛出一个错误。
  • 可以将参数默认值设为 undefined 表明这个参数是可以省略的。

我要回帖

更多关于 golang 参数为函数 的文章

 

随机推荐