Go基础:变量与函数

399 阅读9分钟

本文正在参加金石计划

「前言」

变量

什么是变量

变量——变化的数据。举两个例子:

  1. 假如我们是学生,中午饭点我们饿了,在这种情况下我们第一反应就是去饭堂,可是假如饭堂没有饭,那我们就只能饿肚子,吃不了饭了。在我们去饭堂吃饭的这个例子中,饭就是变量。
  2. 假如我们需要从手机复制一段文字到电脑上,通常最常用的方法就是用电脑与手机登陆同一个微信,然后通过从手机复制到「文件传输助手」上,然后电脑也会接收到信息,此时我们就可以用电脑复制了。在这个过程中,我们利用「文件传输助手」帮助我们在中途存着需要被传输的信息,而它就是一个变量

在以上的例子中,我们能获取变量的两个信息:

  1. 变量是可以变化的;
  2. 变量的作用是以容器的身份作中介。

声明一个变量

数据在电脑里面都是以0 1的形式存在的,这种形式为称之为二进制。计算机的创始人们为了能够简单使用计算机而提出了操作系统,并产生了将数据放起来的思想。随着历史的,发展形成了内存空间的概念——每个数据在系统中以内存空间为单位地被存放起来。可是假如他们没有“独特的名字“,我们想去找一个东西是非常麻烦的,因此为一块内存空间**设置具有逻辑意义的“名字”**是非常重要的。

在编程语言中,变量其实是语言为我们创建的一块内存空间,并且我们被允许去给这些空间”命名”,同时指定这块变量空间放的是什么东西,这个步骤就叫变量声明。

声明,是指先告诉给编译器我们需要什么样的变量,接着编译器会去做准备工作,只有在完成这个步骤之后我们才有机会去使用这个变量。以下是一个最简单的变量:

var msg chan string

var是Go声明变量的关键字,msg是变量的自定义名字,chan string是变量类型。

命名法
  1. 驼峰命名法

    • 小驼峰命名法:除第一个单词之外,其他单词首字母大写。如:变量myStudentCount第一个单词是全部小写,后面的单词首字母大写。
    • 大驼峰命名法:把小驼峰的第一个单词的首字母也大写了。
  2. 大小写确定可见性

    • 以大写字母开头,可以在包外部访问;
    • 以小写字母开头,只能在包内部访问。
变量类型
  1. 基本数据类型/预声明类型

    分类具体类型
    整型int8uint8
    int16uint16
    int32uint32
    int64uint64
    浮点型float32float64
    布尔型bool
    字符型rune
    字符串string
    复数complex64complex128
  2. ⚠️自定义类型:type newType oldType

  3. 未命名类型/类型字面量

    类型名称例子
    数组(array)var numsA [5]int
    切片(slice)var numsB []int
    字典(map)var hashMap map[string]int
    通道(channel)var msg chan string
    结构体(struct)struct{}
    type Person struct {}
    接口(interface)interface {}
    type Worker interface {}
    函数(function)func (){}
    func MethodA() (){}
    func (s something) MethodA()(){}

怎么给它一个值

  1. 给字面常量:字面常量就是类型1“A”Person{ name: "Tom" }这样的常量值。
  2. 给一样类型的变量值:声明方式相同的变量就是相同类型的变量。
  3. 给相似类型的变量值:Go能够隐式相互转换的变量,如string[]byte

函数

什么是函数

当我们在写数学题的时候,题目总会给我们一些已知条件,然后让我们证明某某条件是否正确或者解出某个数据的最终值。此时我们可以把数学题理解为是一个函数,那些题目的已知条件就是参数列表,解题的过程就是我们对已知条件的进一步加工,也可以被称为函数的函数体,而我们所得的最终结果就是函数的返回值

在变量的声明中,我们告诉编译器:“我要声明一个名字是blabla的变量。”那么编译器就会在内存中创建一块空间,里面放的是变量的值,并且为其设置一个名字。那么在方法的声明中,编译器也是先分配一块内存空间并为其设置一个名字,但是不同于变量的是,内存空间里放的是计算机能看得懂的代码,即二进制代码

怎么声明一个函数

函数的结构包括函数名、参数列表、返回值类型和函数体等四部分。其中,函数名是函数的标识符,用于在程序中调用函数。参数列表是一组参数,它们传递给函数的值可以在函数内部使用。返回值类型指定了函数返回值的类型。函数体是包含在大括号内的一段代码——它执行的逻辑。

以下是两种函数的声明结构:

func 函数名(参数列表) (返回值类型) {
    函数体
}

func (参数列表) (返回值类型){
  函数体
}

在上面代码中我们声明了两种函数,而最大的区别就是有没有名字。根据有没有名字这个条件,函数被分成两种——有名函数匿名函数

怎么初始化一个函数变量

函数签名

函数签名也被称为函数的类型。函数可以被看作是一种类型,也就是说函数类型是由一组特定的参数类型和返回值类型所定义的。如下所示:

func add(x int, y int) int {
    return x + y
}

func tranc(str string) (int, int){
  return 0, len(str)
}

在上面例子中,我们定义了两个函数,分别是addtranc。前者接收两个int类型的参数并返回一个int类型的值,则其类型就是func(int, int) int;后者接收一个string类型的参数并返回两个int类型的值,则其类型就是func (string) (int, int)

声明一个函数变量
type CalFuncType func(int, int) int

在上面的例子中,FuncType 是一个函数类型,它接收两个 int 类型的参数并返回一个 int 类型的值。因此,我们可以使用这个函数类型来定义一个变量:

var f CalFuncType

f = add

此时,f 就成为了一个函数类型的变量,并将 add 函数赋值给了 f 变量。

函数有什么用

调用函数
// 计算x+y的值,并返回结果
func add(x int, y int) (int) {
    return x + y
}
r1 := add(1, 1)

// 计算x-y,并返回结果
r2 := func (x int, y int) (int) {
  return x - y
}(2, 1)

f := func (x int, y int) (int) {
  return x * y
}
r3 := f(2, 4)

在以上例子中,我们分别声明了一个名为add的有名函数(普通函数)和一个匿名函数。

  • 对于add的调用,我们只需要直接用方法名( 参数列表 )的方式就能直接调用
  • 对于匿名函数的调用
    • 在声明后添加(参数列表)就能直接调用,然后将返回值赋值给r2
    • 将匿名函数作为变量赋值给f,通过f( 参数列表 )直接调用
⚠️Go的参数传递是值传递

下面看例子:

func ptrFunc (x int){
  fmt.Printf("x:%p\n", &x) // x:0xc0000b2018
}

func main(){
  var x int = 0
  fmt.Printf("x:%p\n", &x) // x:0xc0000b2008
  ptrFunc(x)
}

以上我们定义了一个ptrFunc的方法,其接受一个int类型的参数,并且打印参数的地址。接着我们定义了一个int类型变量x并且初值为0,首先打印一次它的地址,然后传入ptrFunc内部再打印一次地址。这时我们发现两个地址是不一样的,这是因为在Go中,所有参数传递都是值传递——我们将参数传入方法中时,本质上是复制一份到方法里。这样会导致两种情况:

  1. 由于参数数据结构复杂,导致产生额外的开销
  2. 我们在其他函数中无法修改参数本身。

我们可以通过将指针作为参数而改善以上两种情况。

  1. 虽然传入指针的操作也是值的传递,可是指针的数据结构是不变的,并且其本身足够小,因此复制的开销不会改变;
  2. 指针的指向就是某个变量的实际内存空间的地址,因此其具有改变内存空间中内容的能力。
⚠️闭包

先来看一个例子:

func counter() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

func main() {
    c := counter()
    fmt.Println(c()) // 输出 1
    fmt.Println(c()) // 输出 2
    fmt.Println(c()) // 输出 3
}

在这个例子中,counter 函数返回了一个内部函数,这个内部函数引用了外部函数中的变量 i。每次调用返回的内部函数,都会使计数器加一,并返回当前的计数值。因为 i 被保存在返回的内部函数中,所以计数器的值在不同的函数调用之间保持不变。counter中的匿名函数与变量i之间所存在的这种关系叫做闭包。

被称为“闭包”,是因为匿名函数与变量i的这种引用关系将两者封闭了起来,形成了一个封闭的空间使得函数能够访问超过其作用域的变量

假如我们将代码修改成这样:

func counter() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

func main() {
    c1 := counter()
    fmt.Println(c1()) // 输出 1
    fmt.Println(c1()) // 输出 2
    fmt.Println(c1()) // 输出 3
  	c2 := counter()
    fmt.Println(c2()) // 输出 1
    fmt.Println(c2()) // 输出 2
    fmt.Println(c2()) // 输出 3
}

通过这个例子我们会发现,c1c2是两个不同的闭包环境并且互不影响。