golang基础语法学习1 | 青训营

75 阅读14分钟

Go语言基础

入门

go run filename.go

可以将一个或者多个以.go为后缀的源文件进行编译、链接,然后运行生成的可执行文件。

go build filename.go

一般用于不是一次性的实验,编译输出成一个可复用的程序。

:=

用于短变量声明,这个宏语句声明一个或多个变量,并且更具初始化的值给予合适的类型

i++

等价于i += 1,又等价于i = i + 1,对应的递减语句i--同理。这些是语句,不同于C族语句一样是表达式,j = i++是不合法的,并且只支持后缀。

for

Go里面唯一的循环语句

_

空标识符,可以用在任何语法需要变量名但是程序逻辑不需要的地方

程序结构

名称

  • 名称的开头是一个字母或下划线、后面可以跟任意数量的字符、数字和下划线并区分大小写。
  • 风格上采用“驼峰式”。

25个关键字

break:退出循环
​
default:选择结构默认项(switchselect)
​
func:定义函数interface:定义接口
​
select:channel
​
case:选择结构标签
​
chan:定义 channel
​
const:常量
​
continue:跳过本次循环
​
defer:延迟执行内容(收尾工作)
​
go:并发执行
​
mapmap 类型
​
struct:定义结构体
​
else:选择结构
​
goto:跳转语句
​
package:包
​
switch:选择结构
​
fallthrough:流程控制
​
if:选择结构
​
range:从 slice、map 等结构中取元素
​
type:定义类型
​
for:循环
​
import:导入包
​
return:返回
​
var:定义变量

预声明常量、类型和函数

常量:true    false    iota    nil
​
类型:int       int8      int16     int32     int64
​
           uint     uint8    uint16   uint32   uint64    uintptr
​
           float32   float64    complex128      complex64
​
           bool     byte     rune      string    error
​
函数: make    len    cap    new    append    copy    close    delete   
​
            complex     real    imag
​
            panic      recover

声明

4个主要的声明:变量(var),常量(const),类型(type)和函数(func)。

变量

var声明创建一个具体类型的变量。每一个声明都有一个通用的形式:

var name type = expression

类型和表达式部分可以省略一个,但是不能都省略,Go里面不存在未初始化变量。

可以声明一个变量列表,忽略类型允许声明多个不同类型的变量,如:

var i, j, k int     //int, int, int
var b, f, s = true, 2.3, "four"     //bool, float64, string

短变量声明

一种称作短变量声明的可选形式可以用来声明和初始化局部变量。它使用name := expression的形式,name的类型由expression的类型决定。

  • 短变量声明不需要声明所有在左边的变量

在如下代码中,第一条语句声明了inerr。第二条语句仅声明了out,但向已有的err变量赋了值。

in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)
  • 短变量声明最少声明一个变量,否则,代码编译将无法通过。
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // 编译错误:没有新的变量

指针

指针的值是一个变量的地址。使用指针,可以在无须知道变量名字的情况下,间接读取或更新变量的值。

x := 1
p := &x             // p 是整型指针,指向 x 
fmt.Println(*p)     // "1"
*p = 2              // 等于 x = 2
fmt.Println(x)      // 结果 "2"
  • 指针是可比较的,两个指针当且仅当指向同一个变量或者两者都是nil的情况才相等。
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
  • 函数返回局部变量的地址是非常安全的。

例如,通过下面代码,调用函数f产生局部变量v即使在调用返回后依然存在,指针p依然引用它:

var p = f()
func f() *int {
    v := 1
    return &v
}

每次调用f都会返回一个不同的值:

fmt.Println(f() == f()) // "false"

new函数

使用内置的new函数也是创建变量的一种方式。表达式new(T)创建一个未命名的T类型变量,初始化为T类型的零值,并返回其地址(地址类型为*T)。

func newInt() *int {
    return new(int)
}
p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
  • new是一个预声明的函数,不是一个关键字,所以他可以重定义为另外的其他类型。

变量的生命周期

生命周期指在程序执行过程中变量存在的时间段。变量的生命周期是通过它是否可达来确定。

  • 局部变量可在包含它的循环的一次迭代中之外继续存活,即使包含它的循环已经返回,它的存在还可能延续。

C++如果使用new操作申请的内存是分配在堆上的要自己利用delete进行回收,如果是声明的局部变量会在栈上分配内存,并且在函数退出后由系统自动回收。但是Golang在这方面与传统语言发生了非常大的区别,go语言编译器会做逃逸分析(escape analysis),分析局部变量的作用域是否逃出函数的作用域,要是没有,那么就放在栈上;要是变量的作用域超出了函数的作用域,那么就自动放在堆上。所以不用担心会不会memory leak,因为go语言有强大的垃圾回收机制。这样可以释放程序员的内存使用限制,让程序员关注程序逻辑本身。go语言也允许利用new来分配内存,但是new分配的内存也不是一定就放在堆上,而是根据其是否超出了函数作用域来判断是否放在堆上还是栈上。这点和C/C++很不一样。

赋值

赋值语句用来更新变量所指的值,它最简单的形式由赋值符=,以及符号左边的变量和右边的表达式组成。

多重赋值

在实际更新变量前,右边所有表达式被推演,当变量同时出现在赋值符两侧的时候这种形式特别有用。

  • 交换两个变量
x, y = y, x
a[i], a[j] = a[j], a[i]
  • 使一个普通的赋值序列变得紧凑
i, j, k = 2, 3, 5

可赋值性

可赋值性根据类型不同有着不同的规则,类型必须精确匹配,nil可以被赋给任何接口变量或引用类型。

类型声明

type声明定义一个新的命名类型,它和某个已有类型使用相同的底层类型

  • 命名类型提供了一种方式来区分底层类型的不同或者不兼容使用,这样它们就不会在无意中混用。

基本数据

计算机底层全是位,而实际操作则是基于大小固定的单元中的数值,称为字(word)。

  • Golang的二元操作符按优先级的降序排列如下。
*   /   %   <<  >>  &   &^(AND NOT)
+   -   |   ^   
==  !=  <   >   >=  
&&  
||

整数

有符号整数分四种大小:8位、16位、32位、64位,用int8int16int32int64表示,对应的无符号整数是uint8uint16uint32uint64。此外还有intuint

  • rune类型是int32类型的同义词,常常用于指明一个值是Unicode码点。同样,byte类型是uint8类型的同义词,强调一个值是原始数据,而非量值。
  • 无符号整数uintptr其大小并不明确,但足以完整存放指针。
  • 若表示算术运算结果所需的位超出该类型的范围,就称为溢出
  • 比较表达式本身类型是布尔型。
  • 如果将整数以位模式处理,须使用无符号整型。
  • 无符号整数往往只用于位运算和特定算术运算符,如实现位集时,解析二进制格式的文件,或散列和加密。一般而言,无符号整数极少用于表示非负值。
fmt小技巧
  • 通常Printf的格式化字符串含有多个%谓词,这要求提供相同数目的操作数,而%后的副词[1]告知Printf重复使用第一个操作数。
  • %o%x,或%X之前的副词#告知Printf输出相应的前缀00x0X
  • Printf用谓词%b以二进制形式输出数值,副词08能在这个输出结果前补0,补够8位。
  • %c输出文字符号,如果希望输出带有单引号则用%q

浮点数

Go具有两种大小的浮点数float32float64。常量math.MaxFloat32float32的最大值,大约为3.4e38,而math.MaxFloat64则大约为1.8e308。相应地,最小的正浮点值大约位1.4e-454.9e-324

fmt小技巧
  • %g会自动保持足够的精度,并选择最简洁的表达方式,但是对于数据表,%e(有指数)或%f(无指数)的形式可能更合适。

复数

Go具备两种大小的复数complex64complex128,二者分别由float32float64构成。内置的complex函数根据给定的实部和虚部创建复数,而内置的real函数和imag函数则分别提取

  • 源码中,如果在浮点数或十进制整数后面紧接着写字母i,如3.1415926i2i,它就变成了一个虚数,表示一个实部为0的复数。

布尔值

bool型的值或布尔值(boolean)只有两种可能:真(true)和假(false)。

字符串

字符串是不可变的字节序列,它可以包含任意数据,包括0值字节,但主要是人类可读的文本。

  • 不可变一位置两个字符串能安全地共用同一段底层内存,使得复制任何长度字符串的开销都低廉。

字符串字面量

Go的源文件总是按UTF-8编码,并且习惯上Go的字符串会按UTF-8解读。

  • 原生的字符串字面量的书写形式是...,使用反引号而不是双引号。

Unicode

文字符号的序列表示成int32值序列,这种表示方式称作UTF-32UCS-4,每个Unicode码点的编码长度相同,都是32位。

UTF-8

import "unicode/utf8"

UTF-8以字节为单位对Unicode码点做变长编码。

  • 若最高位为0,则标示着它是7为的ASCII码,其文字符号的编码仅占1字节;若最高几位是110,则文字符号的编码占用2个字节,第二个字节以10开始。更长的编码以此类推。

字符串和字节slice

4个标准包对字符串操作特别重要:bytesstringsstrconvunicode

  • 字符串包含一个字节数组,创建后它就无法改变。相反地,字节slice的元素允许随意修改。

strings包具备下面6个函数:

func Contains(s, substr string) bool
func Count(s, sep string) int
func Fields(s string) []string
func HasPrefix(s, prefix string) bool
func Index(s, sep string) int
func Join(a []string, sep string) string

bytes包里面的对应函数为:

func Contains(b, subslice []byte) bool
func Count(s, sep []byte) int
func Fields(s []byte) [][]byte
func HasPrefix(s, prefix []byte) bool
func Index(s, sep []byte) int
func Join(s [][]byte, sep []byte) []byte

bytes包为高效处理字节slice提供了Buffer类型。bytes.Buffer类型无须初始化,原因是零值本来就有效。

  • 若要在bytes.Buffer变量后面添加任意文字符号的UTF-8编码,最好使用bytes.BufferWriteRune方法,而追加ASCII字符,则使用WriteByte亦可。

字符串和数字的相互转换

  • 要将整数转换成字符串,一种选择是使用fmt.Sprintf,另一种做法是用函数strconv.Itoa("integer to ASCII")
  • strconv包内的Atoi函数或ParseInt函数用于解释表示整数的字符串,而ParseUint用于无符号整数。
x, err := strconv.Atoi("123")               // x 是整型
x, err := strconv.ParseInt("123", 10, 64)   // 十进制,最长为64位

`ParseInt`的第三个参数指定结果必须匹配何种大小的整型

常量

常量是一种表达式,其可以保证在编译阶段就计算出表达式的值,并不需要等到运行时,从而使编译器得以知晓其值。

  • 所有常量本质上都属于基本类型:布尔型、字符串或数字。
  • 常量声明可以同时指定类型和值,如果没有显示指定类型,则类型根据右边的表达式推断。
  • 若同时声明一组常量,除了第一项之外,其他项在等号右侧的表达式都可以省略,这一位置会复用前面一项的表达式及其类型。

常量生成器iota

常量声明中,iota从0开始取值,逐项加1。

type Weekday int
const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

无类型常量

从属类型待定的常量共有6种,分别是无类型布尔无类型浮点数无类型复数无类型字符串

  • 类似地,truefalse是无类型布尔值,而字符串字面量则是无类型字符串。
  • 只有常量才可以是无类型的。

复合数据类型

复合数据类型是由基本数据类型以各种方式组合而构成的。

数组

数组是具有固定长度且拥有零个或者多个相同数据类型元素的序列。

  • 相比数组,slice很多场合下使用得更多。
  • 默认情况下,一个新数组中的元素初始值位元素类型的零值,同时可以用数组字面量初始化。
var r [3]int = [3]int{1, 2} // 1, 2, 0
  • 如果省略号...出现在数组长度的位置,那么数组的长度由初始化数组的元素个数决定。
q := [...]int{1, 2, 3} // [3]int
  • 数组的长度是数组类型的一部分。
  • 数组的长度必须是常量表达式,也就是说,这个表达式的值在程序编译时就可以确定。
  • 如果一个数组的元素类型是可比较的,那么这个数组也是可比较的。
  • Go把数组和其他的类型都看成值传递,而别的语言中数组是隐式地使用引用传递。
  • 使用数组指针是高效的,但由于数组长度不可变的特性,很少使用数组。

slice

slice通常写作[]T,其中元素的类型都是T,是一种轻量级的数据结构,可以用来访问数组的部分或者全部元素,这个数组称为slice的底层数组。

  • slice有三个属性:指针,长度和容量。

  • slice操作符s[i:j](其中0ijcap(s))slice操作符s[i:j](其中0\leq i \leq j \leq cap(s))

这个新的slice引用了序列s中从i到j-1索引位置的所有元素,这里的s既可以是数组或者指向数组的指针,也可以是slice。

  • 如果x是字符串,那么x[m:n]返回的是一个字符串;如果x是字节slice,那么返回的结果是字节slice
  • slice包含了指向数组元素的指针,所以将一个slice传递给函数的时候,可以在函数内部修改底层数组的元素。
  • slice字面量看上去和数组字面量很像,都是用逗号分隔并用花括号括起来的一个元素序列,但slice没有指定长度。
s := []int{0, 1, 2, 3, 4, 5}
  • 和数组不同,slice不能用=来测试两个slice是否拥有相同的元素。bytes.Equal()可以用来比较两个字节slice[]byte)。
  • slice唯一允许的比较操作是和nil作比较。值为nilslice没有对应的底层数组。
var s []int     // len(s) == 0, s == nil
s = nil         // len(s) == 0, s == nil
s = []int(nil)  // len(s) == 0, s == nil
s = []int{}     // len(s) == 0, s != nil
  • 检查一个slice是否是空,使用len(s) == 0,而不是s == nil
append函数

内置函数append用来将元素追加到slice的后面。

var runes []rune
for _, r := range "Hello, 世界"{
    runes = append(runes, r)
}
  • 调用append函数的情况下需要更新slice变量。对于任何函数,只要有可能改变slice的长度或者容量,亦或者使得slice指向不同的底层数组,都需要更新slice变量。
  • slice底层数组的元素是间接引用的,但是slice的指针、长度和容量不是。
  • 可以同时给slice添加多个元素,甚至添加另一个slice里的所有元素。
slice就地修改

rotatereverse等可以就地修改slice元素的函数。

map

一个拥有键值对元素的无序集合。在golang中,map是散列表的引用,map的类型是map[K]V,其中kv是字典的键和值对应的数据类型。

  • 内置函数make可以用来创建一个map
mapVal := make(map[string]int)
  • 可以使用内置函数delete来从字典中根据键移除一个元素,即使键不在map中,操作也是安全的:
delete(mapVal, "value") // 移除元素mapVal["value"]
  • map元素不是一个变量,不可以获取它的地址。
  • map顺序是随机的,如果需要按照某种顺序来遍历map中的元素,必须显示地给键排序。如果键是字符串类型,可以使用sort包中的Strings函数来进行键的排序。
  • 通过一种下标方式访问map中的元素输出两个值,第二个值是一个布尔值,用来报告该元素是否存在。
if mapValue, ok := mapValues["val"]; !ok { /* ... */ }