GoLang笔记—基础语法篇

x33g5p2x  于2022-07-04 转载在 其他  
字(17.3k)|赞(0)|评价(0)|浏览(626)

GoLang笔记

Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态类型、编译型语言。Go 语言语法与 C语言相近,但功能上有:内存安全,GC(垃圾回收),结构形态及 CSP-style 并发计算。

一、Go的基本语法

1、变量的声明

Go语言是静态类型语言,因此变量(variable)是有明确类型的,编译器也会检查变量类型的正确性。

我们从计算机系统的角度来讲,变量就是一段或者多段内存,用于存储数据

1.1、标准格式
var 变量名 变量类型

例如: var age int  //声明了一个名为age的变量,类型为int

注意:变量声明以关键字var开头,变量类型后置,行尾无须分号

如果你学过C语言,就会体会到这样声明的好处,比如C语言这样声明:int* a, b ,那么只有a是指针,b不是,这样会使人迷惑,如果想

要两个变量都为指针,需要这样定义:int *a,*b。 在go语言中,我们使用这样的声明方式:var a,b *int,就可以轻松的将a,b都

声明为指针。变量的命名规则遵循驼峰命名法,即首个单词小写,每个新单词的首字母大写,例如: startDate

1.2、基本数据类型
  • bool:布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true
  • string:字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的 [字符数组]
  • int:整型 int 一般占用4个字节、int8、int16、int32(占4个字节)、int64(占8个字节)
  • uint:无符号整数、uint8、uint16、uint32、uint64、uintptr
  • byte:uint8的别名 无符号 8 位整型 (0 到 255)
  • rune:类似int32
  • float32、float64:
  • complex64、complex128:
    有符号和无符号的区别:int8 范围 -128-127,uint8 范围:0-255

**当一个变量被声明之后,系统自动赋予它该类型的零值:**int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil

1.3、不声明变量类型
var level = 1;   //并未声明level的数据类型,go语言编译时会根据所赋的值1,去自动推断level的数据类型

我们可以使用

fmt.Printf("%T", level)   //输出 int , 此处的T就表示的是当前变量的类型
1.4、批量格式类型

觉得每行都用 var 声明变量比较烦琐?Go语言提供了批量声明的方式

var (
    a int
    b string
    c []float32
)

举个例子:

package main

import "fmt"

var (
	a int
	b string
	c float32
)
func main() {
    //%d 整数占位符,%s 字符串占位符, %f 浮点数占位符(默认精度为6)
	fmt.Printf("%d,%s,%f",a,b,c)  //输出: 0, , 0.000000  
}
1.5、简短格式类型

我们可以省略var关键字,这样写起来更便捷

i := 1     //声明一个变量 i 其初始值为1

上面讲过,如果不指明类型,直接赋值,Go会自动推导类型

使用简短格式有以下限制:

  1. 定义变量,同时显式初始化
  2. 不能提供数据类型
  3. 只能用在函数(方法)内部
package main

import "fmt"

//不能
//aa := 1
func main() {
	aa := 1
	fmt.Println(aa)
}

简短变量声明被广泛用于大部分的局部变量的声明和初始化,var 形式的声明语句往往用于需要显式指定变量类型的地方

2、变量的初始化

//方式一:声明并初始化,一般用于声明全局变量
var  level int =  1 

//方式二:短变量声明并初始化,一般是用于声明局部变量
i := 1

以下的代码会出错:

package main

func main() {
	
	var level int = 1
    // 再次声明并赋值 会报错 no new variables on left side of := (左边的变量已经被声明了,不能重复声明)
	level := 1
}

但是有特例

比如:net.Dial提供按指定协议和地址发起网络连接,这个函数有两个返回值,一个是连接对象(conn),一个是错误对象(err)

正常的写法:

package main

import (
	"fmt"
	"net"
)
func main() {

	var conn net.Conn
	var err error
	conn, err = net.Dial("tcp", "127.0.0.1:8080")
	fmt.Println(conn)
	fmt.Println(err)
}

短变量的写法:

package main

import (
	"fmt"
	"net"
)

func main() {

	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	conn1, err := net.Dial("tcp", "127.0.0.1:8080")
	fmt.Println(conn)
	fmt.Println(conn1)
	fmt.Println(err)
}

在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中(与顺序),即便其他变量名可能是重复声明的,编译器也不会报错

3、匿名变量

使用多重赋值时,如果不需要在左值中接受变量,可以使用匿名变量

//向具体网址发起连接请求,如果请求成功,连接对象返回给conn,连接失败将失败对象返回给err
	conn, _ := net.Dial("tcp", "127.0.0.1:8080")  // _表示匿名变量:相当于把这个变量抛弃了,不接收Dial返回的结果
	conn1, _ := net.Dial("tcp", "127.0.0.1:8080") // 匿名变量不可以直接开头

	fmt.Println(conn)     
	fmt.Println(conn1)

匿名变量以“_”下划线表示

匿名变量不占用命名空间,也不会分配内存。匿名变量可以重复声明使用

“_”本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。

4、变量的作用域

一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域

了解变量的作用域对我们学习Go语言来说是比较重要的,因为Go语言(静态语言)会在编译时检查每个变量是否使用过,一旦出现未使用的变量,就会报编译错误。如果不能理解变量的作用域,就有可能会带来一些不明所以的编译错误。

根据变量定义位置的不同,可以分为以下三个类型:

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 函数定义中的变量称为形式参数
4.1、局部变量

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。

局部变量不是一直存在的,它只在定义它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁。

package main
import (
    "fmt"
)
func main() {
    //声明局部变量 a 和 b 并赋值
    var a int = 3
    var b int = 4
    //声明局部变量 c 并计算 a 和 b 的和
    c := a + b
    fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
}
4.2、全局变量

在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,当然,不包含这个全局变量的源

文件需要使用“import”关键字引入全局变量所在的源文件之后才能使用这个全局变量。

全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写

package main
import "fmt"
//声明全局变量
var c int
func main() {
    //声明局部变量
    var a, b int
    //初始化参数
    a = 3
    b = 4
    c = a + b
    fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
}

Go语言程序中全局变量与局部变量名称可以相同,但是函数体内的局部变量会被优先考虑。

package main
import "fmt"

//声明全局变量
var bb float32 = 3.14  

func main() {
	bb := 3   //声明同名局部变量
	fmt.Println(bb)   
}
//执行结果 3
4.3、形式参数

在定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在

函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。

形式参数会作为函数的局部变量来使用

package main

import "fmt"
	a := 1
	b := 2
	c := sum(a, b)
	fmt.Println(c)
}

func sum(a, b int) int {  //a , b 即为形式参数
	return a + b
}
//执行结果 3

至此,Go语言变量相关的知识,我们就掌握了

5、变量的数据类型

5.1、整型

Go语言同时提供了有符号和无符号的整数类型。

  • 有符号整型:int、int8、int64、int32、int64
  • 无符号整型:uint、uint8、uint64、uint32、uint64、uintptr
    有符号整型范围:-2^(n-1) 到 2^(n-1)-1

无符号整型范围: 0 到 2^n-1

实际开发中由于编译器和计算机硬件的不同,int 和 uint 所能表示的整数大小会在 32bit 或 64bit 之间变化。

因此,在二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用 int 和 uint

5.2、浮点型

Go语言支持两种浮点型数:

  1. float32 : 范围 约1.4e-45 到 约3.4e38
  2. float64 :范围约4.9e-324 到 约1.8e308
floatStr1 := 3.2
//保留小数点位数
fmt.Printf("%.2f\n", floatStr1)

算术规范由 IEEE754 浮点数国际标准定义,该浮点数规范被所有现代的 CPU 支持

通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大。

var f float32 = 1 << 24;
fmt.Println(f == f+1) // true

浮点数在声明的时候可以只写整数部分或者小数部分

var e = .71828 // 0.71828
var f = 1.     // 表示我们的f是浮点类型,值为1
fmt.Printf("%.5f,%.1f",e,f)

很小或很大的数最好用科学计数法书写,通过 e 或 E 来指定指数部分

var avogadro = 6.02214129e23  // 阿伏伽德罗常数
var planck   = 6.62606957e-34 // 普朗克常数
fmt.Printf("%f,%.35f",avogadro,planck)
5.3、布尔型

在Go语言中,以bool类型进行声明:

var 变量名 bool

==,>,<<=, >=,&&(AND),||(OR)等都会产生bool值

var aVar = 10
aVar == 5  // false
aVar == 10 // true
aVar != 5  // true
aVar != 10 // false

比较之间的条件:

  • Go语言对于值之间的比较有非常严格的限制,只有两个相同类型的值才可以进行比较,如果值的类型是接口(interface),那么它们也必须都实现了相同的接口。
  • 如果其中一个值是常量,那么另外一个值可以不是常量,但是类型必须和该常量类型相同。
  • 如果以上条件都不满足,则必须将其中一个值的类型转换为和另外一个值的类型相同之后才可以进行比较。

&&(AND),||(OR)是具有短路行为的,如果运算符左边的值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值。(&&优先级高于||)

var a = 10
	//因为a>11已经不满足了,所以a < 30不会走,整个表达式为false
	if(a > 11 && a < 30){
		fmt.Println("正确")
	}else{
		fmt.Println("错误")
	}

	//因为a > 5已经满足了,所以a < 30不会走,整个表达式为true
	if(a > 5 || a < 30){
		fmt.Println("正确")
	}else{
		fmt.Println("错误")
	}

布尔型数据只有true和false,且不能参与任何计算以及类型转换

5.4、字符类型

字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以获得字符。

Go语言的字符有以下两种:

  • 一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
  • 另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。

byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符来说,完全没有问题,例如 var ch byte = 'A',字符使用单引号括起来。而占用两个字节的字符需要使用int32(rune) 例如:var c2 rune = '我'

字符定义的其他方式:

//使用单引号 表示一个字符
var ch byte = 'A'

//在 ASCII 码表中,A 的值是 65,也可以这么定义
var ch byte = 65

//65使用十六进制表示是41,所以也可以这么定义 \x 总是紧跟着长度为 2 的 16 进制数
var ch byte = '\x41'

//65的八进制表示是101,所以使用八进制定义 \后面紧跟着长度为 3 的八进制数
var ch byte = '\101'

fmt.Printf("%c",ch)

Unicode 是 ASCII 的超集,它定义了 1,114,112 个代码点的代码空间。 Unicode 版本 10.0 涵盖 139 个现代和历史文本集(包括符文字母,但不包括 Klingon )以及多个符号集。

Go语言同样支持 Unicode(UTF-8), 用rune来表示, 在内存中使用 int 来表示。

在书写 Unicode 字符时,需要在 16 进制数之前加上前缀\u或者\U。如果需要使用到 4 字节,则使用\u前缀,如果需要使用到 8 个字节,则使用\U前缀。

var ch rune = '\u0041'
	var ch1 int64 = '\U00000041'
	
	//格式化说明符%c用于表示字符,%v或%d会输出用于表示该字符的整数,%U输出格式为 U+hhhh 的字符串。
	 fmt.Printf("%c,%c,%U",ch,ch1,ch)

Unicode 包中内置了一些用于测试字符的函数,这些函数的返回值都是一个布尔值,如下所示(其中 ch 代表字符):

  • 判断是否为字母:unicode.IsLetter(ch)
  • 判断是否为数字:unicode.IsDigit(ch)
  • 判断是否为空白符号:unicode.IsSpace(ch)

UTF-8 和 Unicode 有何区别?
Unicode 与 ASCII 类似,都是一种字符集。

字符集为每个字符分配一个唯一的 ID,我们使用到的所有字符在 Unicode 字符集中都有一个唯一的 ID,例如上面例子中的 a 在 Unicode 与 ASCII 中的编码都是 97。汉字“你”在 Unicode 中的编码为 20320,在不同国家的字符集中,字符所对应的 ID 也会不同。而无论任何情况下,Unicode 中的字符的 ID 都是不会变化的。

UTF-8 是编码规则,将 Unicode 中字符的 ID 以某种方式进行编码,UTF-8 的是一种变长编码规则,从 1 到 4 个字节不等。

编码规则如下:

  • 0xxxxxx 表示文字符号 0~127,兼容 ASCII 字符集。
  • 从 128 到 0x10ffff 表示其他字符。

根据这个规则,拉丁文语系的字符编码一般情况下每个字符占用一个字节,而中文每个字符占用 3 个字节。

广义的 Unicode 指的是一个标准,它定义了字符集及编码规则,即 Unicode 字符集和 UTF-8、UTF-16 编码等。

5.5、字符串类型

一个字符串是 一经定义,一个不可改变的字节序列,字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8

字符的一个序列。 注意:字符串底层是byte数组

字符串的定义:

var mystr string = "hello"

字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容。

当字符为 ASCII 码表上的字符时则占用 1 个字节,如果是unicode编码表上就是3个字节

字符串中可以使用转义字符来实现换行、缩进等效果,常用的转义字符包括:

  1. \n:换行符
  2. \r:回车符
  3. \t:tab 键
  4. \u 或 \U:Unicode 字符
  5. \:反斜杠自身
    如果使用``反引号,会被原样进行赋值和输出
var str = "hello,golang\nNice to meet you"
	var str_1 = `hello,golang
		Nice to meet you`
	
	fmt.Print(str)   // 换行输出
	fmt.Print(str_1)  //同样换行输出

字符串是字节的定长数组,byte 和 rune 都是字符类型,若多个字符放在一起,就组成了字符串

比如 hello ,对照 ascii 编码表,每个字母对应的编号是:104,101,108,108,111

import (
    "fmt"
)

func main() {
    var mystr01 string = "hello"
    var mystr02 [5]byte = [5]byte{104, 101, 108, 108, 111}
    fmt.Printf("myStr01: %s\n", mystr01)
    fmt.Printf("myStr02: %s", mystr02)
}

思考:hello,码神之路 占用几个字节 ?

package main

import (
	"fmt"
)

func main() {
   //中文三字节,字母一个字节
var myStr01 string = "hello,码神之路"
    fmt.Printf("mystr01: %d\n", len(myStr01))  // mystr01:18
}
1、字符串的比较

一般的比较运算符(==、!=、<、<=、>=、>)是通过在内存中按字节比较来实现字符串比较的,因此比较的结果是字符串自然编码的顺序。

字符串所占的字节长度可以通过函数 len() 来获取,例如 len(str)。

字符串的内容(纯字节)可以通过标准索引法来获取,在方括号[ ]内写入索引,索引从 0 开始计数:

  • 字符串 str 的第 1 个字节:str[0]
  • 第 i 个字节:str[i - 1]
  • 最后 1 个字节:str[len(str)-1]

需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效。
注意:获取字符串中某个字节的地址属于非法行为,例如 &str[i]。

ASCII字符使用len()函数

Unicode字符串长度使用utf8.RuneCountInString()函数

//如何计算字符串的长度
    str3 := "hello"
    str4 := "你好"
    fmt.Println(len(str3))  // 1个字母占1个字节
    fmt.Println(len(str4))  // 1个中文占3个字节,go从底层支持utf8
    fmt.Println(utf8.RuneCountInString(str4)) // 2
2、字符串拼接

**方式一:**两个字符串 s1 和 s2 可以通过 s := s1 + s2 拼接在一起。将 s2 追加到 s1 尾部并生成一个新的字符串 s。

//因为编译器会在行尾自动补全分号,所以拼接字符串用的加号“+”必须放在第一行末尾。
	str := "第一部分 " +
		"第二部分"

**方式二:**也可以使用“+=”来对字符串进行拼接:

s := "hel" + "lo,"
s += "world!"
fmt.Println(s) //输出 “hello, world!”

**方式三:**除了使用+进行拼接,我们也可以使用WriteString()

//字符串拼接:方式二(效率会更加高一些)
	s1 := "hello"
	s2 := "golang"
	var stringBuffer bytes.Buffer // 类似JAVA当中的个StringBuffer,相当于一个字符串缓冲区,可以减少字符串的创建次数
	//节省内存分配,提高处理效率
	stringBuffer.WriteString(s1)
	stringBuffer.WriteString(s2)
	fmt.Println(stringBuffer.String())	// hello golang

思考:如果从字符串 hello 语言之王 中获取 该如何获取呢?

str1 := "hello"
	u := str1[0]
	fmt.Printf("%c \n", u) //获取hello中的第一个字节内容

	str2 := "hello 语言之王"
	fmt.Printf("%c \n", str2[0])         //获取str2中的第一个字节内容
	fmt.Printf("%c \n", []rune(str2)[6]) // 先将字符串转换为rune数组,获取数组的第6位
3、字符串便利

str1 := “hello” str2 := “hello 语言之王”

遍历 str1  , str2

//1、传统for循环便利ascii码字符串	str1
for i := 0; i < len(str1); i++ {
		fmt.Printf("%c ", str1[i])
	}

//2、for range 遍历ascii字符串 str1	
for _, s := range str1 {
		fmt.Printf("%c ", s)
	}

//3、for range 遍历unicode字符串	str2
for _, s := range str2 {
		fmt.Printf("%c ", s)
	}
4、字符串的格式化
  • print : 结果写到标准输出
  • Sprint:结果会以字符串形式返回
  • Println:换行输出
  • Printf:按照指定格式输出
res := fmt.Sprintf("%c \n", []rune(str2)[6]) //Sprint会将打印的结果转换为字符串然后返回给res
	fmt.Println(res)
%c  单一字符
%T  动态类型
%v  本来值的输出
%+v 字段名+值打印
%d  十进制打印数字
%p  指针,十六进制
%f  浮点数
%b 二进制
%s string
5、字符串查找

如何获取字符串中的某一段字符?

  • strings.Index(): 正向搜索子字符串
  • strings.LastIndex():反向搜索子字符串
tracer := "go语言天下第一,java表示并不服气"

	//正向搜索字符串
	comma := strings.Index(tracer, ",") //得到,的下标
	fmt.Println(",所在位置:", comma)
	fmt.Println(tracer[comma+1:]) //打印出,以后的所有字符

	add := strings.Index(tracer, "+") //得到 + 的下标 , 如果不存在那么返回的值为-1
	fmt.Println("+所在位置:", add)
6、字符串的修改

Golang语言的字符串是不可变的 ,修改字符串时,可以将字符串转换为[]byte进行修改

[]byte和string可以通过强制类型转换 , 类型转换相关介绍查看:5.6数据类型的转换

案例:案例:将8080改为8081

url := "localhost:8080"
	//url[len(url)-1] = 1   //字符串一经定义无法改变,因此这种方式无法修改
	b := []byte(url)
	b[len(b)-1] = '1' //这里注意是替换的字符1
	url2 := string(b)
	fmt.Println(url2)
5.6、数据类型的转换

在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于Go语言不存在隐式类型转换,因此所有的类型转换都必

须显式的声明 注意:(其数据类型转换的语法规则跟Java的数据类型转换差不多)

//类型 B 的值 = 类型 B(类型 A 的值)
valueOfTypeB = type B(valueOfTypeA)

示例:

a := 5.0
b := int(a)

一些情况

  • 类型转换只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(将 int16 转换为 int32)。
  • 当从一个取值范围较大的类型转换到取值范围较小的类型时(将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失的情况。
  • 只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型)
  • 浮点数在转换为整型时,会将小数部分去掉,只保留整数部分。
小练习

字符串替换, 比如将 “Hello, 码神之路Java教程” 替换为 “Hello, 码神之路Go教程”

思路:

  1. 找到Java所在的位置
  2. 根据Java的长度将其分为两部分
  3. 加上Go总共三部分,进行拼接
// 将Hello,码神之路Java教程 修改为  Hello,码神之路GO教程
	target := "Hello,码神之路Java教程"
	source := "Java"
	newans := "GO"
	pos := strings.Index(target, source)
	start := target[:pos]
	end := target[pos+len(source):]

	var stringBuilder bytes.Buffer //定义一个字符串缓冲区,实现字符串拼接
	stringBuilder.WriteString(start)
	stringBuilder.WriteString(newans)
	stringBuilder.WriteString(end)

	fmt.Println(stringBuilder.String())  // Hello,码神之路GO教程

6、常量

Go语言中的常量使用关键字const定义,用于存储不会改变的数据,常量是在编译时被创建的,即使定义在函数内部也是如此,并且只能是布尔型数字型(整数型、浮点型和复数)和字符串型

由于编译时的限制,定义常量的表达式必须为能被编译器求值的常量表达式。

声明格式:

const name [type] = value

例如:

const pi = 3.14159

type可以省略

和变量声明一样,可以批量声明多个常量:

const (
    e  = 2.7182818
    pi = 3.1415926
)

所有常量的运算都可以在编译期完成,这样不仅可以减少运行时的工作,也方便其他代码的编译优化,当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。

常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len、cap、real、imag、complex 和 unsafe.Sizeof。

因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分

如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式,对应的常量类型也是一样的。例如:

const (
    a = 1
    b
    c = 2
    d
)
fmt.Println(a, b, c, d) // "1 1 2 2"
6.1、iota 常量生成器

常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。

比如,定义星期日到星期六,从0-6

//在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加1
	const (
		Sunday = iota // 0
		Monday
		Tuesday
		Wednesday
		Thursday
		Friday
		Saturday
	)
	fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)  // 0,1,2,3,4,5,6

7、指针

指针(pointer)在Go语言中可以被拆分为两个核心概念:

  • 类型指针:允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。
  • 切片:由指向起始元素的原始指针、元素数量和容量组成。

受益于这样的约束和拆分,Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据

的问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

切片比原始指针具备更强大的特性,而且更为安全。

切片在发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。

7.1、如何理解指针

var a int = 10

  • 如果用大白话来解释上述语句:在内存中开辟了一片空间,空间内存放着数值10,这片空间在整个内存当中,有一个唯一的地址,用来进行标识,指向这个地址的变量就称为指针
  • 如果用类比的说明:内存比作酒店,每个房间就是一块内存,上述代码表示为:定了一间房间a,让10住进了房间,房间有一个门牌号px,这个px就是房间的地址,房卡可以理解为就是指针,指向这个地址。
    一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。

当一个指针被定义后没有分配到任何变量时,它的默认值为 nil

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。

Go语言中使用在变量名前面添加&操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:

package main

import "fmt"

func main() {
	var cat int = 1
	var str string = "Hello,Golang!"
	ptr := &cat  //取出变量cat的地址,赋值给 ptr *int
	fmt.Printf("%p,%p \n", &cat, &str) //打印出变量的内存地址
	fmt.Println(*ptr)                  //取值
}

变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址

当使用&操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*操作符,也就是指针取值

// 指针与变量
	var room int = 10  // room房间 里面放的 变量10
	var ptr = &room  // 门牌号px  指针  0xc00000a0a8

	fmt.Printf("%p\n", &room)  // 变量的内存地址 0xc00000a0a8

	fmt.Printf("%T, %p\n", ptr, ptr)  // *int, 0xc00000a0a8

	fmt.Println("指针地址",ptr)   // 0xc00000a0a8
	fmt.Println("指针地址代表的值", *ptr)  // 10

取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值。
7.2、通过指针修改值

通过指针不仅可以取值,也可以修改值。

package main

func main(){
    // 利用指针修改值
	var num = 10
	modifyFromPoint(num)
	fmt.Println("未使用指针,方法外",num)  // 未使用指针,方法外 10

	var num2 = 22
	newModifyFromPoint(&num2)  // 传入指针
	fmt.Println("使用指针 方法外",num2)    //  使用指针 方法外 10000
}

func modifyFromPoint(num int)  {
	// 未使用指针
	num = 10000
	fmt.Println("未使用指针,方法内:",num)  //未使用指针,方法内 10000
}

func newModifyFromPoint(ptr *int)  {
	// 使用指针
	*ptr = 1000   // 修改指针地址指向的值
	fmt.Println("使用指针,方法内:",*ptr) //  使用指针 方法内 10000
}
7.3、创建指针的另一种方式

Go语言还提供了另外一种方法来创建指针变量,格式如下:

new(类型)   //返回 *类型的指针
ptr01 := new(string) // 等价于 var ptr01 *string = new(string)
	*ptr01 = "Go天下第一"
	fmt.Printf("%s", *ptr01)

new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。

7.4、指针小案例

获取命令行的输入信息

Go语言内置的 flag 包实现了对命令行参数的解析,flag 包使得开发命令行工具更为简单。

package main
// 导入系统包
import (
    "flag"
    "fmt"
)
// 定义命令行参数
var mode = flag.String("mode", "", "fast模式能让程序运行的更快")

func main() {
	// 解析命令行参数
	flag.Parse()
	fmt.Println(*mode)
}

8、变量的生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。(跟Java中变量的生命周期基本一致)

变量的生命周期与变量的作用域有不可分割的联系:

  1. 全局变量:它的生命周期和整个程序的运行周期是一致的;

  2. 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止;

  3. 形式参数和函数返回值:它们都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁。
    go的内存中应用了两种数据结构用于存放变量:

  4. 堆(heap):堆是用于存放进程执行中被动态分配的内存段。它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态加入到堆上(堆被扩张)。当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减);

  5. 栈(stack):栈又称堆栈, 用来存放程序暂时创建的局部变量,也就是我们函数的大括号{ }中定义的局部变量。

栈是先进后出,往栈中放元素的过程,称为入栈,取元素的过程称为出栈。

栈可用于内存分配,栈的分配和回收速度非常快

在程序的编译阶段,编译器会根据实际情况自动选择或者上分配局部变量的存储空间,不论使用 var 还是 new 关键字声明变量都不会影响编译器的选择。

var global *int
func f() {
    var x int
    x = 1
    global = &x
}
func g() {
    y := new(int)
    *y = 1
}

上述代码中,函数 f 里的变量 x 必须在栈上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的。

用Go语言的术语说,这个局部变量 x 从函数 f 中逃逸了。

相反,当函数 g 返回时,变量 y 不再被使用,也就是说可以马上被回收的。因此,y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间,也可以选择在堆上分配,然后由Go语言的 GC(垃圾回收机制)回收这个变量的内存空间。

9、类型别名

类型别名是 Go 1.9 版本添加的新功能,主要用于解决代码升级、迁移中存在的类型兼容性问题。

格式:

//TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型,就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type

还有一种是类型定义:

//定义Name为Type类型 ,定义之后 Name为一种新的类型
type Name Type

类型别名与类型定义表面上看只有一个等号的差异,那么它们之间实际的区别有哪些呢?

package main
import (
    "fmt"
)
// 将NewInt定义为int类型
type NewInt int
// 将int取一个别名叫IntAlias
type IntAlias = int
func main() {
    // 将a声明为NewInt类型
    var a NewInt
    // 查看a的类型名 main.NewInt
    fmt.Printf("a type: %T\n", a)
    // 将a2声明为IntAlias类型
    var a2 IntAlias
    // 查看a2的类型名 int 
    //IntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型。
    fmt.Printf("a2 type: %T\n", a2)
}

10、注释

Go语言的注释主要分成两类,分别是单行注释和多行注释。

  • 单行注释简称行注释,是最常见的注释形式,可以在任何地方使用以//开头的单行注释;
  • 多行注释简称块注释,以/*开头,并以*/结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。

单行注释的格式如下所示

// 单行注释

多行注释的格式如下所示

/*
	第一行注释
	第二行注释
	...
*/

每一个包都应该有相关注释,在使用 package 语句声明包名之前添加相应的注释,用来对包的功能及作用进行简要说明。

同时,在 package 语句之前的注释内容将被默认认为是这个包的文档说明。一个包可以分散在多个文件中,但是只需要对其中一个进行

注释说明即可。

11、关键字和标识符

11.1、关键字

关键字即是被Go语言赋予了特殊含义的单词,也可以称为保留字。

Go语言中的关键字一共有 25 个:

breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

之所以刻意地将Go语言中的关键字保持的这么少,是为了简化在编译过程中的代码解析。

和其它语言一样,关键字不能够作标识符使用。

11.2、标识符

标识符是指Go语言对各种变量、方法、函数等命名时使用的字符序列,标识符由若干个字母、下划线_、和数字组成,且第一个字符必须是字母。下划线_是一个特殊的标识符,称为空白标识符

标识符的命名需要遵守以下规则:

  • 由 26 个英文字母、0~9、_组成;
  • 不能以数字开头,例如 var 1num int是错误的;
  • Go语言中严格区分大小写;
  • 标识符不能包含空格;
  • 不能以系统保留关键字作为标识符,比如 break,if 等等。

命名标识符时还需要注意以下几点:

  • 标识符的命名要尽量采取简短且有意义;
  • 不能和标准库中的包名重复;
  • 为变量、函数、常量命名时采用驼峰命名法,例如 stuName、getVal;

在Go语言中还存在着一些特殊的标识符,叫做预定义标识符,如下表所示:

appendboolbytecapclosecomplexcomplex64complex128uint16
copyfalsefloat32float64imagintint8int16uint32
int32int64iotalenmakenewnilpanicuint64
printprintlnrealrecoverstringtrueuintuint8uintptr

预定义标识符一共有 36 个,主要包含Go语言中的基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用。

12、运算符的优先级

所谓优先级,就是当多个运算符出现在同一个表达式中时,先执行哪个运算符。

Go语言有几十种运算符,被分成十几个级别,有的运算符优先级不同,有的运算符优先级相同,请看下表。

优先级分类运算符结合性
1逗号运算符,从左到右
2赋值运算符=、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|=从右到左
3逻辑或||从左到右
4逻辑与&&从左到右
5按位或|从左到右
6按位异或^从左到右
7按位与&从左到右
8相等/不等==、!=从左到右
9关系运算符<、<=、>、>=从左到右
10位移运算符<<、>>从左到右
11加法/减法+、-从左到右
12乘法/除法/取余*(乘号)、/、%从左到右
13单目运算符!、*(指针)、& 、++、–、+(正号)、-(负号)从右到左
14后缀运算符( )、[ ]、->从左到右

注意:优先级值越大,表示优先级越高。

一下子记住所有运算符的优先级并不容易,还好Go语言中大部分运算符的优先级和数学中是一样的,大家在以后的编程过程中也会逐渐熟悉起来。如果实在搞不清,可以加括号,就像下面这样:

d := a + (b * c)

括号的优先级是最高的,括号中的表达式会优先执行,这样各个运算符的执行顺序就一目了然了。

相关文章