文章由作者马志国在博客园的原创,若转载请于明显处标记出处:http://www.cnblogs.com/mazg/
Go学习群:415660935
2.1 变量
变量是对一块内存空间的命名,程序可以通过定义一个变量来申请一块内存空间。然后可以通过引用变量名来使用这块存储空间。
2.1.1 变量命名
Go语言中的变量名、常量名、类型名、函数名和包名等所有的命名和C语言一样都遵循这样一个简单的命名规则:一个名字必须以一个字母或下划线开头,后面可以跟任意数量的字母、数字或下划线。对于字母区分大小写,例如:name和Name是两个不同的名字。
命名不能与关键字相同,Go语言提供了25个关键字,只能在特定的语法中使用。
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语言还有大约30多个预定义的名字,主要用于内建的常量、类型和函数。这些名字不是关键字,可以重新定义和使用,虽然在一些特殊场景中重新定义是有意义的,但建议尽量不要重新定义,以造成语义混乱问题。
内建常量 |
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 |
2.1.2 变量声明
1 一般声明格式
使用var可以创建一个特定类型的变量。变量声明的一般语法如下:
var 变量名 类型 = 表达式 |
其中“类型”或“=表达式”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型。如果表达式省略,将用零值初始化该变量。
var i int = 1 var j int //j的值初始化为0 var k = 10 // k的类型自动推导 |
如果一个名字在函数内部定义,那么它只能在函数内部使用,如果在函数外部定义,那么在当前包的所有文件都可以访问。名字首字母的大小写决定了它在包外的可见性,首字母大写在包外可以访问。包本身的名字一般总是用小写字母。
在包级别声明的变量会在main入口函数执行之前完成初始化,局部变量将在声明语句被执行的时候完成初始化。如果没有显示初始化,那么将用零值初始化该变量。一个变量声明后没有使用也会引起编译错误。
2 简短变量声明
“名字 := 表达式”,变量的类型根据表达式自动推导。由于使用简单灵活,被广泛用于大部分的局部变量的声明和初始化。注意,简短变量声明不能用于包级别的变量声明。
i := 100 |
3 多个变量声明
var i, j, k int = 1, 2, 3 var m, n int var a, b, c = 1, 2, 3 d, e, f := 1, 2, 3 name, age := "张三", 20 |
也可以这样写:
var ( name string age int ) |
2.1.3 赋值
1 简单赋值
赋值语句是更新一个变量的值,最简单的赋值”变量名= 新值的表达式”
var i int i = 1 //简单赋值 |
2复合赋值运算符
特定的二元算术运算符和赋值语句的复合操作有一个简洁的形式
var i int i = i + 1 i += 1 //与i = i + 1等价 |
数值变量也可以支持++递增和--递减语句。注意它是语句,不是表达式,所以x=i++这样的表达式是错误的。
3多重赋值
多重赋值允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式会先进行求值,然后再统一更新左边对应的变量的值。这样在做两个数值交换时,不需要引入第三个变量了。
x,y=y,x |
4 _标识符
有些表达式会产生多个值,例如调用一个有多个返回值的函数。可以使用下划线空白标识符_来丢弃不需要的值。
_, err := io.Copy(dst, src)//丢弃字节数 |
2.2 常量
在Go语言中,常量是指编译期间就已知且不可改变的值。常量的潜在类型都是基础类型,包括整型、浮点型、复数型、布尔类型和字符串类型等。
2.2.1 字面常量
所谓字面常量,是指程序中硬编码的常量,如:
25 3.14159 2+3i true "hello" |
在其它语言中,常量通常有特定的类型,Go语言的字面常量是无类型的。只要这个常量在相应类型的值域范围内,就可以作为该类型的常量。例如,25可以赋值给int、 uint、int32、int64、float32、float64、complex64、complex128等类型的变量。
2.2.2 常量声明
使用const来声明常量,可以给常量一个友好的名字例如:
const pi = 3.1415926 |
也可以批量声明:
const ( e = 2.7182818 pi = 3.1415926 ) |
一个常量的声明也可以限定类型,但不是必需的。如果没有显示指定类型,那么它与字面量一样,是无类型常量。常量定义的右值也可以是一个在编译期运算的常量表达式,例如:
const i = 1 << 3 //右值是常量表达式 |
如果是批量声明的常量,除第一个外其它的常量的右边的初始化表达式都可以省略,默认使用前面常量的初始化表达式写法。例如:
const ( a = 1 b c = 2 d e ) fmt.Println(a, b, c, d, e) 打印结果:1 1 2 2 2 |
2.2.3 iota常量生成器
Go语言预定义的常量有:true、false和iota,其中iota比较特殊。常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。下面是来自time包的例子。这种定义法在Go语言中通常用于定义枚举值。
type weekday int const( Sunday weekday iota //0 Monday //1 Tuesday //2 Wednesday //3 Thursday //4 Friday //5 Saturday //6 ) |
我们也可以在复杂的常量表达式中使用iota,例如下面每个常量都是1024的幂。
const ( _ = 1 << (10 * iota) KiB MiB GiB TiB ) fmt.Println(KiB, MiB, GiB, TiB) 打印结果:1024 1048576 1073741824 1099511627776 |
总结:var 声明变量,const声明常量。声明时可以带类型。也可以不带类型,通过右推断。
2.3 数据类型
Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。
- 基本数据类型:数值、字符串和布尔型。
- 复合数据类型:数组和结构体。
- 引用类型:指针、切片、字典、函数和通道。
- 接口类型。
2.3.1 整数
1 整数类型
Go语言的数值类型包含了几种不同长度的整数、浮点数和复数。每种数值类型都决定了对应的取值范围和是否支持正负号。
类型 |
长度(字节) |
取值范围 |
int8 |
1 |
(0~255) |
uint8 |
1 |
(-128~127) |
int16 |
2 |
(0~65535) |
uint16 |
2 |
(-32768~32767) |
int32 |
4 |
(-2147483648~2147483647) |
uint32 |
4 |
(0~4294967295) |
int64 |
8 |
(-9223372036854775808~9223372036854775807) |
uint64 |
8 |
(0~18446744073709551615) |
int |
4或8 |
与机器字长和编译器都有关系 |
uint |
4或8 |
与机器字长和编译器都有关系 |
uintptr |
4或8 |
32平台4个字节,64位平台8个字节,底层编程才需要 |
byte |
1 |
与uint8等价,通常表示一个unicode字符编码 |
rune |
4 |
与int32等价,一般强调是一个原始数据而不是一个小整数。在一个字符串中,表示一个字符对应utf8的码点。 |
2 运算符
Go语言提供了丰富的内置运算符,包括算术运算符、比较运算符、逻辑运算符、位运算符、赋值运算符和其它运算符等。
算术运算符:
运算符 |
描述 |
+ |
加 |
- |
减 |
* |
乘 |
/ |
除 |
% |
模运算(求余数) |
++ |
自增 |
-- |
自减 |
在Go语言中,%取模运算符的符号和被取模的符号总是一致的,因此5%3和5%-3的结果都是2。除法运算符的结果则依赖于操作数是否全为整数,例如5.0/4.0的结果是1.25,但是7/4的结果为1,去掉小数部分,而不是四舍五入。
关系(比较)运算符:
两个相同的整数类型可以使用下面的二元关系运算符进行比较,比较表达式的结果是布尔类型。
运算符 |
描述 |
== |
相等 |
!= |
不等 |
< |
小于 |
<= |
小于或等于 |
> |
大于 |
>= |
大于或等于 |
逻辑运算
运算符 |
描述 |
! |
非 |
&& |
与 |
|| |
或 |
位运算:
前4个操作运算符并不区分是有符号还是无符号数:
运算符 |
描述 |
& |
位与and (左侧和右侧都为1,则为1;否则为0) |
| |
位或 or(左侧或右侧只要有一个为1,结果为1;都为0结果才为0) |
^ |
位异或 xor (相同为0,不同为1) |
&^ |
位清空and not(右侧是0,左侧数不变;右侧是1,则左侧数清零) |
<< |
左移 |
>> |
右移 |
位运算的例子:
X=2,y=15 |
二进制结果 |
十进制结果 |
0000 0010 & 0000 1111 |
0000 0010 |
2 |
0000 0010 | 0000 1111 |
0000 1111 |
15 |
0000 0010 ^ 0000 1111 |
0000 1101 |
13 |
0000 0010 &^ 0000 1111 |
0000 0000 |
0 |
0000 0010<<3 |
0001 0000 |
16 |
0000 0010>>1 |
0000 0001 |
1 |
运算符优先级:
2.3.2 浮点数
浮点数用于表示包含小数点的数据。Go语言提供了两种精度的浮点数,float32和float64。float32与float64之间需要强制转换。强制转换的方式T(V),T为要转换的目标类型,V需要转换的变量。
1 浮点数表示
var f1 float32 f1 = 10 f2 := 12.0 //带小数点的自动推导为float64 f2 = float64(f1) //需强制转换 |
2 浮点数比较
因为浮点数不是一种精确的表达方式,所以不能像整型那样直接用==比较。推荐的方式如下,引入math包,计算两个数值之差的绝对值,如果这个结果非常小,我们就认为这两个数值是相等的。至于这个数小到什么程度定义为相等,程序员可以根据项目需求自己定义。
import "math" func IsEqual(f1, f2, p float64) bool { return math.Abs(f1-f2) < p } |
3 科学计数法
把一个数表示成a(1≤a<10,n为整数)与10的幂相乘的形式,这种记数法叫做科学记数法。例如:1990=1.99×10^3。计算器或电脑表达10的幂是一般是用E或e,也就是1.99E3=1990。
f1 := 1.99e+3 //1990 f2 := 1.99e-3 //0.00199 |
2.3.3 复数
Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。内建函数和自然的书写方式。
x := complex(1, 2) //内建函数 y := 1 + 2i //自然书写 // real返回实部,imag返回虚部 fmt.Println(x, y, real(x), imag(x), real(y), imag(y)) 打印结果:(1+2i) (1+2i) 1 2 1 2 |
2.3.4 布尔型
一个布尔类型的值只有两种:true和false。布尔值不会隐式转换为数值0或1。布尔值可以和&&、||操作符结合,并且可能会有短路行为。如果运算符左边已经可以确定整个布尔表达式的值,那么右边的表达式将不再求值。
var s string //s = "mazhiguo" if s != "" && s[0] == 'm' { fmt.Println("OK") } else { fmt.Println("error") } |
2.3.5 字符串
1 字符串常用操作
在Go语言中字符串也是一种基本类型。一个字符串是一个不可改变的字节序列。常用的字符串操作如下表所示:
运算 |
含义 |
备注 |
s1+s2 |
字符串连接 |
|
len(s) |
字符串长度 |
字符串中的字节数,不是字符数 |
s[i] |
取字符 |
索引i不能越界 |
s[i:j] |
取子字符串 |
左闭右开,包含s[i],不包含s[j]。子字符串是一个新的字符串。 i,j都可能被忽略,忽略时,从0开始,最后一个字符结束。 |
s := "hello " + "world" fmt.Println(len(s))// 11 fmt.Println(s[0], s[len(s)-1])//104 100 (h 和 d) fmt.Println(s[1:4])//"ell" fmt.Println(s[:5])//"hello" fmt.Println(s[6:])//"world" fmt.Println(s[:])//"hello world" |
2 字符串值不可变
字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们可以给一个字符串变量分配一个新字符串值。
s := "hello world" s[0] = "H" //这是错误演示,字符串序列不能修改 s = "Hello" //给字符串变量s重新赋值 |
3 字符串遍历
字符串遍历支持以字节的方式遍历和以字符的方式遍历。
s := "hello 世界" n := len(s) //以字节的方式遍历 for i := 0; i < n; i++ { fmt.Println(i, s[i]) } //以字符的方式遍历 for i, ch := range s { fmt.Println(i, ch) } |
打印结果: 0 104 1 101 2 108 3 108 4 111 5 32 6 228 7 184 8 150 9 231 10 149 11 140 0 104 1 101 2 108 3 108 4 111 5 32 6 19990 9 30028 |
4转义序列
在一个双引号包含的字符串字面值中,可以用反斜杠\开头的转义序列插入任意的数据。
常见的ASCII控制代码的转义方式:
\a |
响铃 |
\b |
退格 |
\f |
换页 |
\n |
换行 |
\r |
回车 |
\t |
水平制表符 |
\v |
垂直制表符 |
\’ |
单引号 |
\” |
双引号 |
\\ |
反斜杠 |
5原生字符串字面值
原生的字符串字面值,用` `代替双引号。可用于编写正则表达式。常用于HTML模板、JSON面值、命令提示信息以及需要扩展到多行的场景。
tips := `请按要求执行以下操作: 1 输入参数 2 计算 3 打印结果` fmt.Println(tips) |
6 UTF8编码
UTF8编码是一种字符编码,使用1到4个字节表示一个字符。ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节。变长的编码无法直接通过索引来访问第n个字符。
Go语言的源文件采用UTF8编码,unicode/utf8包提供提供了用于rune字符序列的UTF8编码和解码功能。如果关心每个unicode字符,可以使用UTF8解码器。unicode/utf8包括提供了该功能。
s := "hello 世界" fmt.Println(len(s)) //12 fmt.Println(utf8.RuneCountInString(s))//8 |
将一个整数型转换为字符串意思是生成以只包含对应unicode编码字符的UFT8字符串,如果对应的编码的字符无效,将用‘\uFfFD’无效字符作为替换:
fmt.Println(string(65)) //"A" fmt.Println(string(0x4eac)) //"京" fmt.Println(string(12345678)) //无效字符 |
string 接受到[]rune的类型转换,可以将一个UTF8编码的字符串解码为unicode字符串序列:
s := "世界" fmt.Printf("%x\n", s) //e4b896e7958c,utf8编码 r := []rune(s) fmt.Printf("%x\n", r) //[4e16 754c],unicode编码 |
例如"汉"字的Unicode编码是6C49。6C49在0800-FFFF之间,所以要用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 1100 0100 1001,将这个比特流按三字节模板的分段方法分为0110 110001 001001,依次代替模板中的x,得到:1110-0110 10-110001 10-001001,即E6 B1 89,这就是其UTF8的编码。
s := "汉" fmt.Printf(" %x\n", s) // e6b189,UTF8编码 r := []rune(s) fmt.Printf("%x\n", r) //[6c49],unicode编码 |
unicode相当于字符编码,即字典。utf8、uft16是如何以字节的方式存储这个编码。字符串可比较、可遍历、不可修改。
2.4指针
2.4.1 指针概念
指针是一个类型,该类型的变量称为指针变量。指针变量存储一个变量的地址。它不同于一般的变量,一般变量存放的是数据本身,而指针变量存放的是数据的地址。
2.4.2 声明指针变量
声明指针变量的一般形式如下:
var 变量名 *类型 |
例如:
var ip *int //指向int类型的变量 var fp *float32 //指向float32类型的变量 var ptr [MAX]*int;//指向数组的指针 |
指针操作注意事项:
- 默认值 nil,没有 NULL 常量。
- 操作符 "&" 取变量地址, "*" 通过过指针访问目标对象。
不⽀持指针运算,不⽀持 "->" 运算符,直接⽤ "." 访问目标成员 - 不能对指针做加减法等运算
- 不存在函数的指针
package main import "fmt" type Student struct { Name string Age int } func main() { a := 10 b := 12.5 var pa *int = &a var pb *float64 = &b //1 打印变量的值 fmt.Printf("%v,%v\n", a, b) fmt.Printf("%v,%v\n", *pa, *pb) // 2 打印变量的地址 fmt.Printf("%v,%v\n", &a, &b) fmt.Printf("%v,%v\n", pa, pb) // 3 指针默认值为nil var pc *int fmt.Printf("%v,\n", pc) // 4 通过指针访问对象成员 ps := &Student{"张三", 18} fmt.Println(ps.Name, ps.Age) } |
2.4.3数组指针和指针数组
数组指针是只一个指针变量保存的是数组的地址。指针数组,是指数组的每个元素都是指针类型。
package main import "fmt" func main() { var ptr *[3]int //数组指针 arr := [3]int{1, 2, 3} ptr = &arr //保存了数组的地址 fmt.Println(*ptr) var ptr2 [3]*int //指针数组,每一个元素都是指针 a, b, c := 10, 20, 30 ptr2[0] = &a ptr2[1] = &b ptr2[2] = &c fmt.Println(ptr2) //ptr2数组中的3个指针分别保存了a,b,c的地址 } // 打印结果: [1 2 3] [0xc04200a2c8 0xc04200a2e0 0xc04200a2e8] |
2.4.4 二级指针(多级指针)
二级指针保存一级指针变量的地址。
package main import "fmt" func main() { var a int = 100 var pa *int = &a var ppa **int = &pa //打印a的值 fmt.Printf("%v,%v,%v\n", a, *pa, **ppa) //打印a的地址 fmt.Printf("%v,%v,%v", &a, pa, *ppa) } //打印结果: 100,100,100 0xc0420401d0,0xc0420401d0,0xc0420401d0 |
2.5 type定义类型
在任何程序中都会存在一些变量有着相同的内部结构,但是却表示完全不同的概念。 一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。
type 类型名字 底层类型 |
例如:
type Age int //年龄 type Height int //身高 type Grade int //分数 |
type绝不只是对应于C/C++中的typedef,它不是用于定义一系列的别名。更关键的是,它定义了一系列互不相干的行为特征:通过这些互不相干的行为特征,本质上相同的事物表现出不同事物的特征:整数还是整数,但年龄却不是高度也不是分数。我们可以分别为Age、Height、Grade定 义出下列不同的行为(表示为方法或者函数):
type Age int //年龄 type Height int //身高 type Grade int //分数 func (a Age) IsOld() bool { // 超过50岁算老年 return a > 50 } func (h Height ) NeedTicket() bool { // 高于120cm需要买票 return h > 120 } func (g Grade) Pass() bool { // 60分及格 return g >= 60 } |
类型声明语句一般出现在包一级,因此如果新创建的类型名字首字母大写,则在包外可以使用。对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转换为T类型。
package main import ( "fmt" ) type Integer int func (a Integer) Less(b Integer) bool { return a < b } func main() { var a Integer = 1 fmt.Println(a.Less(2)) var b int //不能直接赋值,需要T(x)类型转换 b = a b = int(a) fmt.Println(b) } |
2.6作用域
一个声明语句将程序中的实体和一个名字关闭,比如一个函数或一个变量。声明语句的作用域是指源代码中可以有效使用这个名字的范围。
不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域,它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内,它可以被程序的其它部分引用,是一个运行时的概念。
语法块是由花括号所包含的一系列语句。语法块内部声明的名字是无法被外部语法块访问的。语句块决定了内部声明的名字的作用域范围。有一个语法块为整个源代码,称为全局语法块;然后是每个包的包语法块;每个for、if和switch语句的语法块;每个switch或select的分支也有独立的语法块;当然也有显示书写的语法块(花括号包含的语句)。
声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,例如int、len和true等都是全局作用域的;任何在函数外部声明的名字可以在包的任何源文件中访问,是包级作用域。对于导入的包,则是对应源文件级的作用域。控制流标号,就是break、continue或goto语句后跟着的那种标号,是函数级作用域。
当编译器遇到一个名字引用时,如果它是一个声明,首先从最内层的作用域向全局作用域查找。如果查找失败,则错误。如果名字在内部和外部分别声明过,则内部块的声明首先被找到,它会屏蔽外部同名的声明。
2.7 数据输入输出
2.7.1 标准输出函数
Print( )函数采用默认格式将其参数格式化并写入标准输出。如果两个相邻的参数都不是字符串,会在它们的输出之间添加空格。返回写入的字节数和遇到的任何错误。函数原型如下:
Println( )与Print( )函数的功能基本一致,唯一不同的是在输出结束后,自动增加换行。函数原型如下:
Printf()函数根据format参数生成格式化的字符串并写入标准输出。返回写入的字节数和遇到的任何错误。函数原型如下:
func Printf(format string, a ...interface{}) (n int, err error) |
2.7.2 标准输入函数
Scan( )函数从标准输入扫描文本,将成功读取的空白分隔的值保存进成功传递给本函数的参数。换行视为空白。返回成功扫描的条目个数和遇到的任何错误。如果读取的条目比提供的参数少,会返回一个错误报告原因。函数原型如下:
Scanln类似Scan,但会在换行时停止扫描。最后一个条目后必须有换行或者到达结束位置。函数原型如下:
Scanf从标准输入扫描文本,根据format 参数指定的格式将成功读取的空白分隔的值保存进成功传递给本函数的参数。返回成功扫描的条目个数和遇到的任何错误。函数原型如下:
func Scanf(format string, a ...interface{}) (n int, err error) |