欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《GO语言圣经》读书笔记(一):程序结构

程序员文章站 2024-02-17 15:30:22
...


​ 变量和常量是编程中必不可少的部分,也是很好理解的一部分。

标识符与关键字

标识符

​ Go中的变量名、常量名、函数名都遵循一个命名规则:一个名字由字母数字和_(下划线)组成,并且只能以字母和_开头,区分大小写。 举几个例子:abc, _, _123, a123

​ 如果一个变量实在函数内部定义的,那么它的作用域就是在整个函数内部,如果在函数外部定义,那么他的作用域就是当前包的所有文件都可以访问,这个名字是个包级别的名字。

​ 除此之外,要特别说的,如果一个名字的是大写字母开头的包级名,那么它是可以被外部的包访问的,如果是小写的他们只有内部包可以访问。举个例子,我们常用的Printf函数是fmt包中可以被外部包访问的函数。

​ 推荐使用驼峰式命名。

关键字

​ 关键字是指编程语言中预先定义好的具有特殊含义的标识符。 关键字和保留字都不建议用作变量名,关键字只能在特定语法结构中使用。

​ Go语言中有25个关键字:

    break        default      func         interface    select
    case         defer        go           map          struct
    chan         else         goto         package      switch
    const        fallthrough  if           range        type
    continue     for          import       return       var

​ 此外,Go语言中还有37个保留字。

    Constants:    true  false  iota  nil

        Types:    int  int8  int16  int32  int64  
                  uint  uint8  uint16  uint32  uint64  uintptr
                  float32  float64  complex128  complex64
                  bool  byte  rune  string  error

    Functions:   make  len  cap  new  append  copy  close  delete
                 complex  real  imag
                 panic  recover

变量

​ 每个源文件以包的声明语句开始,说明该源文件是属于哪个包,之后是import导入依赖的其他包,然后是包一级的类型、变量、常量和函数的声明语句,包一级的各种类型的声明语句的顺序无关紧要。那么首先来看一下变量是如何声明的。

变量的来历

​ 程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。

变量类型

​ 变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型、浮点型、布尔型等。

​ Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。

变量声明

​ Go语言主要有四种类型的声明语句,分别是var/const/type/func,他们对应了变量、常量、类型和函数实体对象的声明。现在,我们来看看如何声明变量。

​ Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用。

标准声明

​ Go语言的变量声明格式为:

var 变量名 变量类型

​ 变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。 举个例子:

var name string
var age int
var isOk bool

批量声明

​ 每声明一个变量就需要写var关键字会比较繁琐,go语言中还支持批量变量声明:

var (
    a string
    b int
    c bool
    d float32
)

变量的初始化

​ Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil。数组或结构体等聚合类型的零值是每个元素/字段对应类型的零值。所以,Go中不存在未初始化的变量。

​ 当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:

var 变量名 类型 = 表达式

​ 举个例子:

var name string = "咕叽咕叽"
var age int = 18

​ 或者一次初始化多个变量

var name, age = "咕叽咕叽", 20

类型推导

​ 有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。

var name = "咕叽咕叽"
var age = 18

短变量声明

​ 在函数内部,可以使用更简略的 := 方式声明并初始化变量。

package main

import (
	"fmt"
)
// 全局变量m
var m = 100

func main() {
	n := 10
	fmt.Println(m, n)
}

​ 变量m是在包一级的变量,变量n是在main内部声明的,m可以在整个包对应的每个文件中被访问到,而不仅仅是当前文件中访问。包级别声明的变量m会在main入口函数执行前完成初始化,对于局部变量n在声明语句快要被执行的时候完成初始化。

​ 短变量声明这种声明方式,变量的类型会根据表达式来自动推导,和var相比,var比较适合需要显式指定变量类型的地方,或者是变量稍后会被重新赋值而初始值不太重要的地方。

​ 请看下面的例子:

in,err:=os.Open(infile)
//do something
out,err:=os.Create(outfile)

​ 第一句,我们通过短变量声明的方式声明了inerr两个变量,在第二个语句中,声明了几个变量呢?哈哈哈,只声明了out一个变量噢,对于之前声明过的err,只是做了赋值操作。

​ 对比一下下面这个例子:

file,err:=os.Open(infile)
//do something
file,err:=os.Create(outfile)  //compile error

​ 你会发现,编译无法通过,咦,这是为什么呢?因为简短变量声明语句中要求必须、至少要声明一个新的变量,然鹅无论是file还是err我们在第一句代码中已经声明过了,所以会出现CE状况。解决方法就是改成多重赋值的语句:

file,err=os.Create(outfile) 

这里来小结以下刚刚对于短变量声明的使用:

  • 短变量声明不适用于声明包级别的变量,只适合在函数内部使用,对于声明包级别的变量还是使用var来完成吧

  • 如果在相同的作用域中声明过该变量了,那么简短变量声明语句会对这些已经声明过的变量进行赋值操作,而不是声明噢。如果变量名虽然相同,但两个同名的变量不在同一个作用域,那么使用简短变量声明相当于在当前作用域重新声明一个新变量。显然下面的这个例子就证实了两个x不是同一个变量。

    package main
    
    import (
    	"fmt"
    )
    
    var x int64
    
    func main() {
        //这里在main函数内重新声明了一个新的变量x
    	x := "hello"
        fmt.Println(x)	//print:hello
    }
    
  • 简短变量声明语句中必须至少要声明一个新的变量,否则编译不通过

匿名变量

​ 在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_表示,例如:

func foo() (int, string) {
	return 10, "Q1mi"
}
func main() {
	x, _ := foo()
	_, y := foo()
	fmt.Println("x=", x)
	fmt.Println("y=", y)
}

​ 匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua等编程语言里,匿名变量也被叫做哑元变量。

注意事项:

  1. 函数外的每个语句都必须以关键字开始(var、const、func等)
  2. :=不能使用在函数外。
  3. _多用于占位,表示忽略值。

PS:一组变量也可以通过函数返回的多个返回值进行初始化,如下:

//该函数的返回值类型为file和error
var f,err=os.Open(name)

指针

​ 一个指针的值是另一个变量的值,一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字。

​ 通过短变量声明,x变量的类型为int&x表达式的意思是取x变量的内存地址,这一操作会产生一个指向该整数变量的指针,指针的类型是*int,所以p是一个指向变量x的指针,或者理解为p指针保存了x变量的内存地址。

​ 接着,我们取出指针所指向变量的值,然后打印,*p表达式对应p指针指向的变量的值。

​ 我们将新的值赋给了*p,因为*p对应一个变量,所以出现在赋值语句的左侧是合理的,这样一来,我们更新了p指针所指向的变量的值,相当于x=2

x:=1
p:=&x	//&x表示取x的内存地址,p是一个指向*int类型的指针,
fmt.Println(*p)	//打印1
*p=2
fmt.Println(x)	//打印2

​ 对于结构体的每一个字段或者数组的每一个元素来说,都是可以被取地址的,因为他们可以看作是一个变量。

​ 任何类型的指针的零值都是nil,如果p!=nil为真,那么说明p指针指向某个有效变量。指针和指针之间,当他们指向同一个变量或者全部是nil时才相等。

var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false

&x&y对应着不同的内存地址,所以&x==&y结果为falsex指向一个有效变量(有内存地址,初始值为0),所以和nil自然不相等。

其他:

  • 我们对一个变量取地址,或者是赋值指针,其实都是为原变量创建了新的别名。比如说p:=&x*p其实就是变量x的别名。有没有发现,通过这种方式,我们可以不使用名字而去访问x变量。

new函数

​ 另一个创建变量的方法就是调用new函数。

//new(int)创建了一个int类型的匿名变量,初始化为0,返回的是地址,所以p是*int类型,指向匿名的int变量
p:=new(int)

​ 每一次调用new函数返回的都是不同变量的地址,p:=new(T)这种方式等价于:

func newInt() *int {
	return new(int)
} 
func newInt() *int {
	var dummy int
	return &dummy
}

new并不是一个关键字,我们可以将new名字重新定义成别的类型。比如我们将它定义为int类型的变量名,然后作为delta函数的参数:

func delta(old,new int)int{
    return new-old;
}

​ 注意,这样的情况,我们在该函数内部是无法使用new函数的。

变量的生命周期

​ 之前我们提到了包一级声明的变量和局部变量,前者的声明周期和整个程序的运行周期是一致的,而局部变量的声明周期是动态的,从每次创建一个新的变量的声明语句开始,一直到该变量不再被引用位置,然后变量的存储空间可能被回收。

判断一个变量何时可以被回收的方法是可达性分析。从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或者引用的访问路径遍历,是否可以找到该变量。如果不存在这样的路径,说明不可达,那么该变量就要被清除掉了。

​ 像函数的入参和返回值都是局部变量,它们在函数被调用时创建。

​ 还要多说一点,编译器是自动选择在栈上或者堆上发呢配局部变量的内存,和如何声明变量的方式无关。下面看一个例子。

var global *int
//f函数中的变量x在堆上分配,因为在函数结束后依然可以通过包级别变量去访问到,尽管是在函数内部定义的
func f() {
	var x int
	x = 1
	global = &x
} 
//g函数中的变量*y在函数结束后将会是不可达的,编译器可以选择栈上或者堆上分配存储空间
func g() {
	y := new(int)
	*y = 1
}

​ 上述代码中出现的f函数中出现了x变量逃逸的现象,因为在f函数结束后,依然可以通过变量global去找到x,实际上局部变量x属于短生命周期对象,但是在f函数中,我们将它的指针保存到了global对象(长生命周期的对象)中,这会阻止对短声明周期对象的垃圾回收。

作用域和声明周期不是一个概念!

  • 声明语句的作用域对应的是一个源代码的文本区域,是一个编译时属性

  • 一个变量的生命周期是指程序运行时变量存在的有效时间段,是一个运行时概念

类型

​ 任何程序中都会存在一些变量有着相同的内部结构,但是却表示完全不同的概念。比如说,一个int变量可以表示一个循环的迭代索引或者一个月份。

​ 我们可以通过一个类型声明语句创建一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使他们底层类型相同也是不兼容的。

类型声明语句

​ 类型声明语句出现在包一级,因此如果新创建的类型名字的首字符大写,那么包外也可以使用。

type 类型名字 底层类型

​ 我们声明两个类型Celsius和Fahrenheit:

type Celsius float64		//摄氏温度
type Fahrenheit float64		//华式温度

​ 这两个类型虽然底层类型都是float64,但是属于不同的数据类型,所以是不可以相互比较或者混在一个表达式中进行运算的。因此,两个类型需要进行显式转型操作才可以进行运算。类型转化不会改变值本身,但是会使语义发生变化。

func CToF(c Celsius) Fahrenheit{
    return Fahrenheit(c*9/5+32)
}

func FToC(f Fahrenheit) Celsius{
    return Celsius((f-32)*5/9)
}

​ Celsius(t)和Fahrenheit(t)是类型转换操作,并不是函数噢。对于每一个类型T,都有一个对应的类型转换操作T(x),可以将x转换为T类型,如果T是指针类型,可能会需要用小括号包装T

​ 只有两个类型的底层基础类型相同的时候,才可以转型,或者两者都是指向相同底层结构的指针类型,这些转换只会改变类型而不会影响值本身。

​ 命名类型可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们成为类型的方法集。

包的初始化

​ 包的初始化是解决包级别变量的依赖顺序,然后按照包级变量声明出现的顺序依次进行初始化。

var a = b + c // a 第三个初始化, 为 3
var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1 // c 第一个初始化, 为 1
func f() int { return c + 1 }

​ 如果一个包中有多个.go文件,那么会将.go文件根据文件名排序,然后依次调用编译器编译。

​ 对于包级别声明的变量,我们可以通过一个特殊的init函数来简化初始化工作,一个文件中可以包含多个init初始化函数。

​ 这个函数很特殊,它不可以被调用和引用,程序开始执行时按照每个文件中的init函数生命的顺序被自动调用。

​ 每个包只会初始化一次,初始化的工作是自下而上的,main包是最后被初始化的,这样我们可以确保main函数之前,所有依赖的包都完成了初始化工作。

导入包的注意事项:

  • 当包被导入的时候,包内的成员将通过类似包名.变量名的形式访问。
  • 如果导入了一个包,但是又没有使用该包,者会被当作一个编译错误处理。

if else(分支结构)

if条件判断基本写法

​ Go语言中if条件判断的格式如下:

if 表达式1 {
    分支1
} else if 表达式2 {
    分支2
} else{
    分支3
}

​ 当表达式1的结果为true时,执行分支1,否则判断表达式2,如果满足则执行分支2,都不满足时,则执行分支3。 if判断中的else ifelse都是可选的,可以根据实际需要进行选择。

​ Go语言规定与if匹配的左括号{必须与if和表达式放在同一行,{放在其他位置会触发编译错误。 同理,与else匹配的{也必须与else写在同一行,else也必须与上一个ifelse if右边的大括号在同一行。

举个例子:

func ifDemo1() {
	score := 65
	if score >= 90 {
		fmt.Println("A")
	} else if score > 75 {
		fmt.Println("B")
	} else {
		fmt.Println("C")
	}
}

if条件判断特殊写法

if条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,举个例子:

func ifDemo2() {
	if score := 65; score >= 90 {
		fmt.Println("A")
	} else if score > 75 {
		fmt.Println("B")
	} else {
		fmt.Println("C")
	}
}

思考题: 上下两种写法的区别在哪里?

for(循环结构)

Go 语言中的所有循环类型均可以使用for关键字来完成。

for循环的基本格式如下:

for 初始语句;条件表达式;结束语句{
    循环体语句
}

条件表达式返回true时循环体不停地进行循环,直到条件表达式返回false时自动退出循环。

func forDemo() {
	for i := 0; i < 10; i++ {
		fmt.Println(i)
	}
}

for循环的初始语句可以被忽略,但是初始语句后的分号必须要写,例如:

func forDemo2() {
	i := 0
	for ; i < 10; i++ {
		fmt.Println(i)
	}
}

for循环的初始语句和结束语句都可以省略,例如:

func forDemo3() {
	i := 0
	for i < 10 {
		fmt.Println(i)
		i++
	}
}

这种写法类似于其他编程语言中的while,在while后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。

无限循环

for {
    循环体语句
}

for循环可以通过breakgotoreturnpanic语句强制退出循环。

for range(键值循环)

Go语言中可以使用for range遍历数组、切片、字符串、map 及通道(channel)。 通过for range遍历的返回值有以下规律:

  1. 数组、切片、字符串返回索引和值。
  2. map返回键和值。
  3. 通道(channel)只返回通道内的值。

switch case

使用switch语句可方便地对大量的值进行条件判断。

func switchDemo1() {
	finger := 3
	switch finger {
	case 1:
		fmt.Println("大拇指")
	case 2:
		fmt.Println("食指")
	case 3:
		fmt.Println("中指")
	case 4:
		fmt.Println("无名指")
	case 5:
		fmt.Println("小拇指")
	default:
		fmt.Println("无效的输入!")
	}
}

Go语言规定每个switch只能有一个default分支。

一个分支可以有多个值,多个case值中间使用英文逗号分隔。

func testSwitch3() {
	switch n := 7; n {
	case 1, 3, 5, 7, 9:
		fmt.Println("奇数")
	case 2, 4, 6, 8:
		fmt.Println("偶数")
	default:
		fmt.Println(n)
	}
}

分支还可以使用表达式,这时候switch语句后面不需要再跟判断变量。例如:

func switchDemo4() {
	age := 30
	switch {
	case age < 25:
		fmt.Println("好好学习吧")
	case age > 25 && age < 35:
		fmt.Println("好好工作吧")
	case age > 60:
		fmt.Println("好好享受吧")
	default:
		fmt.Println("活着真好")
	}
}

fallthrough语法可以执行满足条件的case的下一个case,是为了兼容C语言中的case设计的。

func switchDemo5() {
	s := "a"
	switch {
	case s == "a":
		fmt.Println("a")
		fallthrough
	case s == "b":
		fmt.Println("b")
	case s == "c":
		fmt.Println("c")
	default:
		fmt.Println("...")
	}
}

输出:

a
b

goto(跳转到指定标签)

goto语句通过标签进行代码间的无条件跳转。goto语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto语句能简化一些代码的实现过程。 例如双层嵌套的for循环要退出时:

func gotoDemo1() {
	var breakFlag bool
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				// 设置退出标签
				breakFlag = true
				break
			}
			fmt.Printf("%v-%v\n", i, j)
		}
		// 外层for循环判断
		if breakFlag {
			break
		}
	}
}

使用goto语句能简化代码:

func gotoDemo2() {
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				// 设置退出标签
				goto breakTag
			}
			fmt.Printf("%v-%v\n", i, j)
		}
	}
	return
	// 标签
breakTag:
	fmt.Println("结束for循环")
}

break(跳出循环)

break语句可以结束forswitchselect的代码块。

break语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的forswitchselect的代码块上。 举个例子:

func breakDemo1() {
BREAKDEMO1:
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				break BREAKDEMO1
			}
			fmt.Printf("%v-%v\n", i, j)
		}
	}
	fmt.Println("...")
}

continue(继续下次循环)

continue语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for循环内使用。

​ 在 continue语句后添加标签时,表示开始标签对应的循环。例如:

func continueDemo() {
forloop1:
	for i := 0; i < 5; i++ {
		// forloop2:
		for j := 0; j < 5; j++ {
			if i == 2 && j == 2 {
				continue forloop1
			}
			fmt.Printf("%v-%v\n", i, j)
		}
	}
}
相关标签: Golang