首页 > 编程语言 >Go语言核心36讲 04 | 程序实体的那些事儿(上)

Go语言核心36讲 04 | 程序实体的那些事儿(上)

时间:2024-01-21 19:47:02浏览次数:57  
标签:name 04 代码 36 类型 Go 声明 变量

我已经为你打开了Go语言编程之门,并向你展示了“程序从初建到拆分,再到模块化”的基本演化路径。

一个编程老手让程序完成基本演化,可能也就需要几十分钟甚至十几分钟,因为他们一开始就会把车开到模块化编程的道路上。我相信,等你真正理解了这个过程之后,也会驾轻就熟的。

上述套路是通用的,不是只适用于Go语言。但从本篇开始,我会开始向你介绍Go语言中的各种特性以及相应的编程方法和思想。


我在讲解那两种源码文件基本编写方法的时候,声明和使用了一些程序实体。你也许已经若有所觉,也许还在云里雾里。没关系,我现在就与你一起梳理这方面的重点。

还记得吗?Go语言中的程序实体包括变量、常量、函数、结构体和接口。 Go语言是静态类型的编程语言,所以我们在声明变量或常量的时候,都需要指定它们的类型,或者给予足够的信息,这样才可以让Go语言能够推导出它们的类型。

在Go语言中,变量的类型可以是其预定义的那些类型,也可以是程序自定义的函数、结构体或接口。常量的合法类型不多,只能是那些Go语言预定义的基本类型。它的声明方式也更简单一些。

好了,下面这个简单的问题你需要了解一下。

**问题:声明变量有几种方式?

**

先看段代码。

package main

import (
	"flag"
	"fmt"
)

func main() {
	var name string // [1]
	flag.StringVar(&name, "name", "everyone", "The greeting object.") // [2]
	flag.Parse()
	fmt.Printf("Hello, %v!\n", name)
}

这是一个很简单的命令源码文件,我把它命名为demo7.go。它是demo2.go的微调版。我只是把变量name的声明和对flag.StringVar函数的调用,都移动到了main函数中,这分别对应代码中的注释[1][2]

具体的问题是,除了var name string这种声明变量name的方式,还有其他方式吗?你可以选择性地改动注释[1][2]处的代码。

典型回答

这有几种做法,我在这里只说最典型的两种。

第一种方式需要先对注释[2]处的代码稍作改动,把被调用的函数由flag.StringVar改为flag.String,传参的列表也需要随之修改,这是为了[1][2]处代码合并的准备工作。

var name = flag.String("name", "everyone", "The greeting object.")

合并后的代码看起来更简洁一些。我把注释[1]处的代码中的string去掉了,右边添加了一个=,然后再拼接上经过修改的[2]处代码。

注意,flag.String函数返回的结果值的类型是*string而不是string。类型*string代表的是字符串的指针类型,而不是字符串类型。因此,这里的变量name代表的是一个指向字符串值的指针。

关于Go语言中的指针,我在后面会有专门的介绍。你在这里只需要知道,我们可以通过操作符*把这个指针指向的字符串值取出来了。因此,在这种情况下,那个被用来打印内容的函数调用就需要微调一下,把其中的参数name改为*name,即:fmt.Printf("Hello, %v!\n", *name)

好了,我想你已经基本理解了这行代码中的每一个部分。

**下面我接着说第二种方式。**第二种方式与第一种方式非常类似,它基于第一种方式的代码,赋值符号=右边的代码不动,左边只留下name,再把=变成:=

name := flag.String("name", "everyone", "The greeting object.")

问题解析

这个问题的基本考点有两个。一个是你要知道Go语言中的类型推断,以及它在代码中的基本体现,另一个是短变量声明的用法。

第一种方式中的代码在声明变量name的同时,还为它赋了值,而这时声明中并没有显式指定name的类型。

还记得吗?之前的变量声明语句是var name string。这里利用了Go语言自身的类型推断,而省去了对该变量的类型的声明。

简单地说,类型推断是一种编程语言在编译期自动解释表达式类型的能力。什么是表达式?详细的解释你可以参看Go语言规范中的表达式表达式语句章节。我在这里就不赘述了。

你可以认为,表达式类型就是对表达式进行求值后得到结果的类型。Go语言中的类型推断是很简约的,这也是Go语言整体的风格。

它只能用于对变量或常量的初始化,就像上述回答中描述的那样。对flag.String函数的调用其实就是一个调用表达式,而这个表达式的类型是*string,即字符串的指针类型。

这也是调用flag.String函数后得到结果的类型。随后,Go语言把这个调用了flag.String函数的表达式类型,直接作为了变量name的类型,这就是“推断”一词所指代的操作了。

至于第二种方式所用的短变量声明,实际上就是Go语言的类型推断再加上一点点语法糖。

我们只能在函数体内部使用短变量声明。在编写ifforswitch语句的时候,我们经常把它安插在初始化子句中,并用来声明一些临时的变量。而相比之下,第一种方式更加通用,它可以被用在任何地方。

(变量的多种声明方式)

短变量声明还有其他的玩法,我稍后就会讲到。

知识扩展

**1. Go语言的类型推断可以带来哪些好处?

**

如果面试官问你这个问题,你应该怎样回答?

当然,在写代码时,我们通过使用Go语言的类型推断,而节省下来的键盘敲击次数几乎可以忽略不计。但它真正的好处,往往会体现在我们写代码之后的那些事情上,比如代码重构。

为了更好的演示,我们先要做一点准备工作。我们依然通过调用一个函数在声明name变量的同时为它赋值,但是这个函数不是flag.String,而是由我们自己定义的某个函数,比如叫getTheFlag

package main

import (
	"flag"
	"fmt"
)

func main() {
	var name = getTheFlag()
	flag.Parse()
	fmt.Printf("Hello, %v!\n", *name)
}

func getTheFlag() *string {
	return flag.String("name", "everyone", "The greeting object.")
}

我们可以用getTheFlag函数包裹(或者说包装)那个对flag.String函数的调用,并把其结果直接作为getTheFlag函数的结果,结果的类型是*string

这样一来,var name =右边的表达式,可以变为针对getTheFlag函数的调用表达式了。这实际上是对“声明并赋值name变量的那行代码”的重构。

我们通常把不改变某个程序与外界的任何交互方式和规则,而只改变其内部实现”的代码修改方式,叫做对该程序的重构。重构的对象可以是一行代码、一个函数、一个功能模块,甚至一个软件系统。

好了,在准备工作做完之后,你会发现,你可以随意改变getTheFlag函数的内部实现,及其返回结果的类型,而不用修改main函数中的任何代码。

这个命令源码文件依然可以通过编译,并且构建和运行也都不会有问题。也许你能感觉得到,这是一个关于程序灵活性的质变。

我们不显式地指定变量name的类型,使得它可以被赋予任何类型的值。也就是说,变量name的类型可以在其初始化时,由其他程序动态地确定。

在你改变getTheFlag函数的结果类型之后,Go语言的编译器会在你再次构建该程序的时候,自动地更新变量name的类型。如果你使用过PythonRuby这种动态类型的编程语言的话,一定会觉得这情景似曾相识。

没错,通过这种类型推断,你可以体验到动态类型编程语言所带来的一部分优势,即程序灵活性的明显提升。但在那些编程语言中,这种提升可以说是用程序的可维护性和运行效率换来的。

Go语言是静态类型的,所以一旦在初始化变量时确定了它的类型,之后就不可能再改变。这就避免了在后面维护程序时的一些问题。另外,请记住,这种类型的确定是在编译期完成的,因此不会对程序的运行效率产生任何影响。

现在,你应该已经对这个问题有一个比较深刻的理解了。

如果只用一两句话回答这个问题的话,我想可以是这样的:Go语言的类型推断可以明显提升程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担(实际上,它恰恰可以避免散弹式的代码修改),更不会损失程序的运行效率。

**2. 变量的重声明是什么意思?

**

这涉及了短变量声明。通过使用它,我们可以对同一个代码块中的变量进行重声明。

既然说到了代码块,我先来解释一下它。在Go语言中,代码块一般就是一个由花括号括起来的区域,里面可以包含表达式和语句。Go语言本身以及我们编写的代码共同形成了一个非常大的代码块,也叫全域代码块。

这主要体现在,只要是公开的全局变量,都可以被任何代码所使用。相对小一些的代码块是代码包,一个代码包可以包含许多子代码包,所以这样的代码块也可以很大。

接下来,每个源码文件也都是一个代码块,每个函数也是一个代码块,每个if语句、for语句、switch语句和select语句都是一个代码块。甚至,switchselect语句中的case子句也都是独立的代码块。

走个极端,我就在main函数中写一对紧挨着的花括号算不算一个代码块?当然也算,这甚至还有个名词,叫“空代码块”。

回到变量重声明的问题上。其含义是对已经声明过的变量再次声明。变量重声明的前提条件如下。

  1. 由于变量的类型在其初始化时就已经确定了,所以对它再次声明时赋予的类型必须与其原本的类型相同,否则会产生编译错误。

  2. 变量的重声明只可能发生在某一个代码块中。如果与当前的变量重名的是外层代码块中的变量,那么就是另外一种含义了,我在下一篇文章中会讲到。

  3. 变量的重声明只有在使用短变量声明时才会发生,否则也无法通过编译。如果要在此处声明全新的变量,那么就应该使用包含关键字var的声明语句,但是这时就不能与同一个代码块中的任何变量有重名了。

  4. 被“声明并赋值”的变量必须是多个,并且其中至少有一个是新的变量。这时我们才可以说对其中的旧变量进行了重声明。

这样来看,变量重声明其实算是一个语法糖(或者叫便利措施)。它允许我们在使用短变量声明时不用理会被赋值的多个变量中是否包含旧变量。可以想象,如果不这样会多写不少代码。

我把一个简单的例子写在了“Golang_Puzzlers”项目的puzzlers/article4/q3包中的demo9.go文件中,你可以去看一下。

这其中最重要的两行代码如下:

var err error
n, err := io.WriteString(os.Stdout, "Hello, everyone!\n")

我使用短变量声明对新变量n和旧变量err进行了“声明并赋值”,这时也是对后者的重声明。

总结

在本篇中,我们聚焦于最基本的Go语言程序实体:变量。并详细解说了变量声明和赋值的基本方法,及其背后的重要概念和知识。我们使用关键字var和短变量声明,都可以实现对变量的“声明并赋值”。

这两种方式各有千秋,有着各自的特点和适用场景。前者可以被用在任何地方,而后者只能被用在函数或者其他更小的代码块中。

不过,通过前者我们无法对已有的变量进行重声明,也就是说它无法处理新旧变量混在一起的情况。不过它们也有一个很重要的共同点,即:基于类型推断,Go语言的类型推断只应用在了对变量或常量的初始化方面。

思考题

本次的思考题只有一个:如果与当前的变量重名的是外层代码块中的变量,那么这意味着什么?

这道题对于你来说可能有些难,不过我鼓励你多做几次试验试试,你可以在代码中多写一些打印语句,然后运行它,并记录下每次试验的结果。如果有疑问也一定要写下来,答案将在下篇文章中揭晓。

戳此查看Go语言专栏文章配套详细代码。

标签:name,04,代码,36,类型,Go,声明,变量
From: https://www.cnblogs.com/rskd/p/17978219/go-yu-yan-he-xin36jiang-04--cheng-xu-shi-ti-de-na

相关文章

  • Go语言核心36讲 06 | 程序实体的那些事儿 (下)
    在上一篇文章,我们一直都在围绕着可重名变量,也就是不同代码块中的重名变量,进行了讨论。还记得吗?最后我强调,如果可重名变量的类型不同,那么就需要引起我们的特别关注了,它们之间可能会存在“屏蔽”的现象。必要时,我们需要严格地检查它们的类型,但是怎样检查呢?咱们现在就说。我今天......
  • Go语言核心36讲 05 | 程序实体的那些事儿(中)
    在前文中,我解释过代码块的含义。Go语言的代码块是一层套一层的,就像大圆套小圆。一个代码块可以有若干个子代码块;但对于每个代码块,最多只会有一个直接包含它的代码块(后者可以简称为前者的外层代码块)。这种代码块的划分,也间接地决定了程序实体的作用域。我们今天就来看看它们之......
  • 对CF1904C的代码优化
    https://www.luogu.com.cn/problem/CF1904C分讨,然后\(k=2\)的时候肯定要写暴力,但是我的暴力很不优雅。石山voidsolve(){intn,k;cin>>n>>k;vector<ll>a(n+1);for(inti=1;i<=n;i++)cin>>a[i];if(k>=3){......
  • P5362 [SDOI2019] 连续子序列 题解--zhengjun
    题面传送门提供一种和其他题解完全不同的解法。记\(P_0\)为题中给出的序列,\(P_1\)为\(P_0\)取反的结果。记\(S_{l\simr}\)表示\(S_lS_{l+1}\dotsS_{r}\)。方便起见,\(P\)下标从\(0\)开始,其余的串都是从\(1\)开始。这里用\(P_{0,i}=\operatorname{popcount(i)}......
  • Github图床搭建,结合Picgo与jsdelivr的免费cdn加速,以及部分问题解决方案
    留份文档,便于后续查询===================用到的地址:Github:GitHubPicgo:PicGoisHere|PicGojsdelivr加速地址:https://cdn.jsdelivr.net/gh/Github用户名/仓库名@master===================1.创建一个GitHub仓库:进入你的GitHub首页,在右上角你会找到一个➕,在下拉菜单中......
  • 【LeetCode】704. 二分查找
    题目:704.二分查找解题思路思路:给定一个nums数组,注意数组是升序排列的;那么,找到当前target元素是否存在于数组之中,可采用二分法查找注意:此处定义该数组区间定义:【左闭右闭】/【左闭右开】使用left指向数组头,right指针指向数组尾,mid用于计算二分查找的位置,mid=left+(ri......
  • uniapp+django登录页面实现
    前后端联动概述以一个简单的登录功能为例说明,uni-app的前后端交互项目地址:效果图前端页面开发项目地址:后端页面开发项目地址:其他参考资料1、Django项目和uni-app项目的创建及项目文件讲解https://blog.csdn.net/qq_55002406/article/details/1287887882、如何将前......
  • Godot中鼠标点击3D对象
    Godot中鼠标点击3D对象方法一:调用RigidBody3D中的input_event事件RigidBody3D中有信号input_event可以接受鼠标的输入,用这个信号可以处理点击事件。 具体用法如下:新建一个demo场景连接上信号input_event代码如下:判断是否有鼠标点击事件即可。 publicvoid_on_rig......
  • 算法学习Day36重叠区间
    Day36重叠区间ByHQWQF2024/01/21笔记435.无重叠区间给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。注意:可以认为区间的终点总是大于它的起点。区间[1,2]和[2,3]的边界相互“接触”,但没有相互重叠。示例1:输入:[[1,2],[2,3],[3,4],[1......
  • 【Dynamics365-Finance&Operations学习】Chain of Command Feature使用方法与使用场景
    前提微软在PlatformUpdate9之后引入了ChainofCommand(CoC),通过支持像Public和Protected类型的拓展,来为技术顾问和编程人员减少过度分层(overlayering)。在PU15(Dynamic365的某一版本)中,在Form、Table和Class的CoC已经被实现,但在表单数据源(FormDataSource)和表单数据字段(Formdat......