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


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

标识符与关键字

标识符

​ 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)
		}
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值