道阻且长,行则将至,行而不辍,未来可期🌟。人生是一条且漫长且充满荆棘的道路,一路上充斥着各种欲望与诱惑,不断学习,不断修炼,不悔昨日,不畏将来!
GO语言也被称为21世纪的C语言,在开发与性能效率上都占据优势(Python+C)🚀。让我们一起来了解这门语言的魅力吧,希望这篇能够带给你们或多或少的帮助!!

一、变量的使用

变量指定了某存储单元(Memory Location)的名称,该存储单元会存储特定类型的值,变量的关键在于变,这也意味着它代表并不是一个固定不变的数据。在 Go 中,有多种语法用于声明变量。


1.1 定义变量


在所有语言内,遍历必须是先声明,后使用的。而部分语言在声明变量的时候需要指定变量类型,这是区分静态与动态语言的关键。

1、变量声明方式:指定变量类型

变量定义语法:

var 变量名 变量类型 = 变量值

变量声明方式:

package mainimport "fmt"func main() {var age int // 声明并未赋值,int类型变量值默认为0// 注意:Go语言中变量定义了必须使用,否则报错fmt.Println(age)age = 18 // 赋值fmt.Println(age)var width int = 180 // 变量声明并赋值fmt.Println(width)// var width int 已经声明的变量不能重复声明,否则报错
}

以上代码输出结果:

0
18
180

2、变量声明方式:类型推断 (Type Inference)


如果变量有初始值,那么 Go 能够自动推断具有初始值的变量的类型。因此,如果变量有初始值,就可以在变量声明中省略 type。

如果变量声明的语法是 var name = initialvalue,Go 能够根据初始值自动推断变量的类型。

在下面的例子中,我们省略了变量 age 的 int 类型,Go 依然推断出了它是 int 类型。

package mainimport "fmt"func main() {var age = 18fmt.Println(age)
}

3、声明多个变量


Go 能够通过一条语句声明多个变量。

声明多个变量的语法是 var name1, name2 = initialvalue1, initialvalue2

package mainimport "fmt"func main() {var age1 ,age2 = 18,28fmt.Println(age1,age2)// 声明多个变量但是不赋值var a1,b1 int // 这就表示a/b变量都是int类型fmt.Println(a1,b1)// 声明多个变量,且类型不同var (a2 intb2 string)fmt.Println(a2,b2)// 声明多个变量,且赋值var (name = "jack"height = 180)fmt.Println(name,height)
}

至于string类型,我们将会在下面说明

上面程序运行结果如下:

18 28
0 0
0 
jack 180

4、简短声明


:=

声明变量的简短语法是 name := initialvalue,省略var关键字,类型由Go自行推断出来

package mainimport "fmt"func main() {age := 10fmt.Println(age)// 多个变量使用简短声明a,b := 1,2fmt.Println(a,b)
}

简短声明要求 := 操作符左边的所有变量都有初始值。下面程序将会抛出错误 cannot assign 1 values to 2 variables,这是因为 age 没有被赋值。

package mainimport "fmt"func main() {  name, age := "naveen" // errorfmt.Println("my name is", name, "age is", age)
}

简短声明的语法要求 := 操作符的左边至少有一个变量是尚未声明的。考虑下面的程序:

package mainimport "fmt"func main() {a, b := 20, 30 // 声明变量a和bfmt.Println("a is", a, "b is", b)b, c := 40, 50 // b已经声明,但c尚未声明fmt.Println("b is", b, "c is", c)b, c = 80, 90 // 给已经声明的变量b和c赋新值,注意:没有冒号!!!fmt.Println("changed b is", b, "c is", c)
}

在上面程序中,由于 b 已经被声明,而 c 尚未声明,因此运行成功并且输出:

a is 20 b is 30
b is 40 c is 50
changed b is 80 c is 90

但是如果我们运行下面的程序:

package mainimport "fmt"func main() {  a, b := 20, 30 // 声明a和ba, b := 40, 50 // 错误,:=左边没有尚未声明的变量
}

上面运行后会抛出 no new variables on left side of := 的错误,这是因为 a 和 b 的变量已经声明过了,:= 的左边并没有尚未声明的变量。


cannot use "naveen" (type string) as type int in assignment
package mainfunc main() {  age := 29      // age是int类型age = "naveen" // 错误,尝试赋值一个字符串给int类型变量
}

1.2 常量

常量:顾名思义就是经常使用的变量。

在Go语言内,定义常量一经赋值就是不可改变的,而在其它语言内,如Python:通常是以大写字母来表示常量,但是由于Python比较自由,所以只是在约定方面建议不更改,但实际是可以改变的。

Go语言常量定义:

package mainfunc main() {// 定义常量(在函数内定义只能在当前函数作用域使用)const ip = "127.0.0.1"// ip = "0.0.0." 不可以改变常量的值,会产生报错
}

1.3 变量的赋值与内存相关

关于这部分内容,深入理解对我们理解程序的底层会更有帮助。

0x7ffcad3b8f3c
a变量0x00001

变量名的本质是什么?

这是很多学习编程的人容易忽略的问题,他们只关注了变量名代指内存中某一块数据,却忽略了变量名是否也在内存中存储?那么它的地址又是什么?

实际上变量名就是为了让我们编程时更加方便,对人友好,可计算机可不认识什么变量 a,它只知道地址和指令。所以实际在编译后,变量名都被取而代之为其对应的内存地址

变量名 -> 内存地址

也就是有这样一个映射表存在,将变量名自动转化为地址:

a:0x000001
b:0x000002
c:0x000003

当我们了解以上内容的时候,对于变量的理解已经到位了。那么下面我们来看看Go语言中变量的赋值,以及内存的一些变化。

package mainimport "fmt"func main() {a := 10fmt.Println(a) // a这个变量关联了一个内存地址,调用这个a变量等于通过这个地址去内存中取值fmt.Println(&a) // 在变量前面增加&符号,查看其关联的内存地址
}

打印结果:

10
0xc00001c068

如果我们修改数据的话,内存地址是不变的,根据这个地址去覆盖其原有的值

package mainimport "fmt"func main() {name := "Jack"fmt.Println(name, &name)name = "Tom" // 修改name变量的值(根据内存地址,去到所在内存位置覆盖掉其原有的值)fmt.Println(name, &name)
}

输出结果:

Jack 0xc00008e1e0
Tom 0xc00008e1e0

当一个变量指向另外一个变量时,它们的内存地址并不相同,只是将数据复制到了一块新的内存空间进行存储

package mainimport "fmt"func main() {name := "Jack"fmt.Println(name, &name)nickname := namefmt.Println(nickname, &nickname)
}

输出结果:

Jack 0xc000010200
Jack 0xc000010220 // 不完全相同

这与Python是有差异的,在Python中,如果一个变量指向另外一个变量,等同于复用了它的内存地址。

name = "jack"
nick_name = nameprint(id(name))
print(id(nick_name))

输出结果:

140417703147248
140417703147248

关于Go语言的变量赋值,注意事项:

使用:int、string、bool这三种数据类型时,会将数据拷贝一份到到新的内存地址存储。也就是我们上面第三个实例的情况:name = “jack”;nickname = name

后续还会学习到其它数据类型,我们到时再来分析它们之间的关系;这些数据类型又分为两类:

  • 值类型:上面三种数据类型属于值类型,每个数据都是独立的,不会被复用
  • 引用类型:后续会了解到

二、变量基本类型


下面是 Go 支持的基本类型:

数字类型:

  • int8, int16, int32, int64, int
  • uint8, uint16, uint32, uint64, uint
  • float32, float64
  • complex64, complex128
  • byte
  • rune

string(字符串类型):Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。

bool(布尔类型):布尔型的值只可以是常量 true 或者 false。

派生类型:我们留到后续讲解


2.1 有符号整型

有符号数可以用来区分正负。而无符号数只有正数,没有负数。


int8:表示 8 位有符号整型
大小:8 位
范围:-128~127

iint16:表示 16 位有符号整型
大小:16 位
范围:-32768~32767

iint32:表示 32 位有符号整型
大小:32 位
范围:-2147483648~2147483647

iint64:表示 64 位有符号整型
大小:64 位
范围:-9223372036854775808~9223372036854775807

int:根据不同的底层平台(Underlying Platform),表示 32 或 64 位整型。除非对整型的大小有特定的需求,否则你通常应该使用 int 表示整型。

大小:在 32 位系统下是 32 位,而在 64 位系统下是 64 位。

-2147483648~2147483647 -9223372036854775808~9223372036854775807
package mainimport "fmt"func main() {  var a int = 89b := 95fmt.Println("value of a is", a, "and b is", b)fmt.Printf("%T",b)
}

上面程序会输出如下结果:

value of a is 89 and b is 95
int

在上述程序中,a 是 int 类型,而 b 的类型通过赋值(95)推断得出。上面我们提到,int 类型的大小在 32 位系统下是 32 位,而在 64 位系统下是 64 位。接下来我们会证实这种说法。

在 Printf 方法中,使用 %T 格式说明符(Format Specifier),可以打印出变量的类型。Go 的 unsafe 包提供了一个 Sizeof 函数,该函数接收变量并返回它的字节大小。unsafe 包应该小心使用,因为使用 unsafe 包可能会带来可移植性问题。不过出于本教程的目的,我们是可以使用的。

下面程序会输出变量 a 和 b 的类型和大小。格式说明符 %T 用于打印类型,而 %d 用于打印字节大小。

package mainimport ("fmt""unsafe"
)func main() var a int = 89b := 95fmt.Println("value of a is", a, "and b is", b)fmt.Printf("type of a is %T, size of a is %d", a, unsafe.Sizeof(a)) // a 的类型和大小fmt.Printf("\ntype of b is %T, size of b is %d", b, unsafe.Sizeof(b)) // b 的类型和大小
}

以上程序会输出以下结果:

value of a is 89 and b is 95
type of a is int, size of a is 8
type of b is int, size of b is 8

从上面的输出,我们可以推断出 a 和 b 为 int 类型,且大小都是 32 位(4 字节)。如果你在 64 位系统上运行上面的代码,会有不同的输出。在 64 位系统下,a 和 b 会占用 64 位(8 字节)的大小。

不同整型之间是不能直接使用“算术运算符”的,例如:

package mainfunc main() {var i1 int8 = 10var i2 int16 = 10v3 := int16(16) + i2 // 只有两个数据类型相同才能进行相加
}
int8int16
  • 低位可以往位置转换(int8 -> int16)
  • 高位往低位转可能会出现问题(int16 -> int8)

在Golang里面高位往地位转的话,当数据值大于低位的范围以后,则会从低位的起始开始往前递增

package mainimport "fmt"func main() {// int8范围:-128 - 127var i2 int16 = 128fmt.Println(int8(i2))// 打印:-128,128大于int8的范围后,则回到最起始var i3 int16 = 129fmt.Println(int8(i2))// 打印:-127,129大于int8的范围后,则回到最起始+1// 如果是130,转换为int8以后则是-126
}

2.2 无符号整型


uint8:表示 8 位无符号整型
大小:8 位
范围:0~255

uint16:表示 16 位无符号整型
大小:16 位
范围:0~65535

uint32:表示 32 位无符号整型
大小:32 位
范围:0~4294967295

uint64:表示 64 位无符号整型
大小:64 位
范围:0~18446744073709551615

uint:根据不同的底层平台,表示 32 或 64 位无符号整型。

大小:在 32 位系统下是 32 位,而在 64 位系统下是 64 位。

范围:在 32 位系统下是 0~4294967295,而在 64 位系统是 0~18446744073709551615。


2.3 string类型


在 Golang 中,字符串是字节的集合。如果你现在还不理解这个定义,也没有关系。我们可以暂且认为一个字符串就是由很多字符组成的。我们后面会在一个教程中深入学习字符串。 下面编写一个使用字符串的程序。

package mainimport (  "fmt"
)func main() {  first := "Naveen"last := "Ramanathan"name := first +" "+ lastfmt.Println("My name is",name)
}

上面程序中,first 赋值为字符串 “Naveen”,last 赋值为字符串 “Ramanathan”。+ 操作符可以用于拼接字符串。我们拼接了 first、空格和 last,并将其赋值给 name。

上述程序将打印输出:

My name is Naveen Ramanathan

还有许多应用于字符串上面的操作,我们将会在一个单独的教程里看见它们。


2.4 bool类型


bool 类型表示一个布尔值,值为 true 或者 false。表示正确、错误的

package mainimport "fmt"func main() {a := 10b := 20fmt.Println(a > b)fmt.Println(a < b)
}

在上面的程序中,a 赋值为 10,b 赋值为 20。

a 大于 bfalsea 小于 btrue
false
true
truefalse
package mainimport "fmt"func main() {auth := false
}

三、输出


输出指的是将我们需要展示出来的内容打印到控制台。而输出也经常作用于:

  • 程序运行的结果是否符合我们的预期
  • 清除程序运行的进度
  • 代码调试(也可以用于Debug代替)

在之前的程序里面,我们已经实际接触到了Go语言内输出相关的代码

import fmtfunc main(){fmt.Println("Hello Golang")
}
fmtHello Golang

3.1 常用打印功能

这里我们主要使用比较常用的打印功能,主要是换行与不换行的区别。

package mainimport "fmt"func main() {fmt.Println("能够换行的输出1")fmt.Println("能够换行的输出2") // 下一次打印的内容将会在这一行的下面fmt.Print("不能够换行1")fmt.Print("不能够换行2") // 下一次打印的内容将会和这一个行内容并排fmt.Println("Hello", "Golang") // 在同一个输出内,我们可以通过 , 来进行多个内容拼接。这个行为可以搭配后续学习到的变量一起使用fmt.Print("Hello\nWorld") // 我们也可以通过转义符内的:\n换行符来实现指定位置换行的一个效果
}

上面效果打印:


3.2 格式化输出

,
%s%d%%%%%t%s
,
fmt.Printf

实例:

package mainimport "fmt"func main() {// 而在下面实例中,我们需要注意的是,%%是不需要数据填充的,我们仅作为能够打印出%的用途fmt.Printf("数字占位符%%d:%d \n", 123)fmt.Printf("浮点类型占位符%%f:%f \n", 3.1415926)fmt.Printf("布尔类型占位符%%t:%t \n", true)fmt.Printf("字符串占位符%%s:%s \n", "Hello Golang")fmt.Printf("我的名字是:%s;今年:%d岁;薪资:%0.1fw,相比去年上涨了80%%", "Jack", 20, 3.5)// 上面在完整内容的结尾处,我们通过逗号,按顺序对占位符进行对应数据类型填充值// 我们也可以观察到,浮点类型我们使用的%0.1f,这表示只保留小数点后1位,默认是6位// 对%s也可以是:%0.1s,这表示只保留填充值的第一个字符,Jack -> J
}

上面实例结果:


3.3 内置输出方法与fmt的区别

printprintln
package mainfunc main() {println("output1")print("output2")
}
fmt
  • 内置的Println()/Print():都是标准错误输出,而fmt.Println()函数是标准输出
  • 内置的Println()/Print():输出结果可能与预期结果顺序不一致,而fmt.Println()函数输出结果与预期结果完全一致。(这个特性是由标准错误输出和标准输出决定)
  • 内置Println()/Print():不能接受数组和结构体类型的数据
  • 内置Println()/Print():对于组合类型的数据输出的结果是参数值的地址,而fmt.Println()函数输出的是字面量

重点说一下标准错误输出和标准输出的区别;

  • 标准错误输出:在Linux中是stderr,在Java中是System.err,在Golang中是Print()/Println()
  • 标准输出:在Linux中是stdout,在Java中是System.out,在Golang中是fmt.Println()

其实从字面意思上就能看出,一个是专为输出错误用的,一个是通常输出用的,都是输出流。使用内置函数 println 和 print 的好处显而易见:不用引用任何包,并且代码编译更简单;但是也有一定的风险,因为 Go 语言官方文档中声明的这几个函数一般用于内部测试,不保证随着 Go 语言的版本升级会始终提供。一旦不提供,将意味着大量的代码修改工作,因此建议大家使用 fmt 包中的 Println 或 Print 函数。


四、注释


//

注释也分为单行注释与多行注释

  1. 单行注释:主要多用于对某段代码进行解释
  2. 多行注释:对功能函数、文件、模块等大范围的内容进行一个作用说明

多行注释语法:

/*该文件主要对用户数据进行处理
*/

五、运算符


运算符用于在程序运行时执行数学或逻辑运算。

这里介绍几个我们在Go语言内最常用的几个内置运算符:

  • 算术运算符
  • 关系运算符
  • 逻辑运算符
  • 赋值运算符

算术运算符

下表列出了所有Go语言的算术运算符。假定 A 值为 10,B 值为 20。

关系运算符

下表列出了所有Go语言的关系运算符。假定 A 值为 10,B 值为 20。

逻辑运算符

关系运算符与逻辑运算符在条件判断中最为常用

赋值运算符

下表列出了所有Go语言的赋值运算符。


六、条件语句


6.1 基本使用

if 是条件语句。if 语句的语法是

if condition {  // 触发条件执行的代码块
}

条件语句的作用是,当满足什么样的条件,执行什么样的代码。这就和人思考一样,满足了有钱的条件下,才能去买房、买车、旅游…

conditiontrue{}
{ }{ }
else ifelse
if condition {  // 代码块
} else if condition {// Code
} else {// 当上面的if else if 不成立的时候才执行的Code
}
if-elseelse ififelse ifelse

让我们编写一个简单的程序来检测一个数字是奇数还是偶数。

package mainimport (  "fmt"
)func main() {  num := 10if num % 2 == 0 { //checks if number is evenfmt.Println("the number is even") }  else {fmt.Println("the number is odd")}
}
if num%2 == 0the number is even
ifstatement 
if statement; condition {  // Code
}

让我们重写程序,使用上面的语法来查找数字是偶数还是奇数。

package mainimport (  "fmt"
)func main() {  // 使用if的同时声明了num变量if num := 10; num % 2 == 0 { // checks if number is evenfmt.Println(num,"is even") }  else {fmt.Println(num,"is odd")}
}
numifnumifelsenumif elseifelsenum
else if 
package mainimport (  "fmt"
)func main() {  num := 99if num <= 50 {fmt.Println("number is less than or equal to 50")} else if num >= 51 && num <= 100 {fmt.Println("number is between 51 and 100")} else {fmt.Println("number is greater than 100")}}
else if num >= 51 && num <= 100number is between 51 and 100

需要注意的地方:

elseif}

让我们通过以下程序来理解它。

package mainimport (  "fmt"
)func main() {  num := 10if num % 2 == 0 { //checks if number is evenfmt.Println("the number is even") }  else {fmt.Println("the number is odd")}
}
elseif} 
main.go:12:5: syntax error: unexpected else, expecting }

出错的原因是 Go 语言的分号是自动插入。

}}

实际上我们的程序变成了

if num%2 == 0 {  fmt.Println("the number is even") 
};  //semicolon inserted by Go
else {  fmt.Println("the number is odd")
}

分号插入之后。从上面代码片段可以看出第三行插入了分号。

 if{…} else {…}else}
elseif}
package mainimport (  "fmt"
)func main() {  num := 10if num % 2 == 0 { //checks if number is evenfmt.Println("the number is even") } else {fmt.Println("the number is odd")}
}

此时的if编写才符合Go语言的规范。


6.2 条件嵌套

任何条件语句之内都可以继续写条件语句。无限套娃!当然这样的写法也只是为了满足我们的功能需求,以及增加代码的可读性。

package mainimport "fmt"func main() {money := 1500000if money > 0 {if money > 7000000 {fmt.Println("700w买房、买车")} else if money > 5000000 {fmt.Println("500w买房")} else if money > 2000000 {fmt.Println("200w买车")} else {fmt.Println("小于200w,距离梦想太遥远,继续赚钱")}} else {fmt.Println("口袋空空如也,赶紧去打工!")}
}
if else if...

七、函数(Function)


函数是什么?

函数是一块执行特定任务的代码。一个函数是在输入源基础上,通过执行一系列的算法,生成预期的输出。


7.1 函数的声明

在 Go 语言中,函数声明通用语法如下:

func functionname(parametername type) returntype {  // 函数体(具体实现的功能)
}
funcfunctionname (函数名)()returntype (返回值类型)(parameter1 type, parameter2 type) 即(参数1 参数1的类型,参数2 参数2的类型){}

函数中的参数列表和返回值并非是必须的,所以下面这个函数的声明也是有效的

func functionname() {  // 译注: 表示这个函数不需要输入参数,且没有返回值
}

实例

我们以写一个计算商品价格的函数为例,输入参数是单件商品的价格和商品的个数,两者的乘积为商品总价,作为函数的输出值。

// 这两个参数我们可以称之为:位置参数;因为我们调用传递是根据位置来进行的
func calculateBill(price int, no int) int {  var totalPrice = price * no // 商品总价 = 商品单价 * 数量return totalPrice // 返回总价
}// 如:calculateBill(10,20) 10这个数据根据位置传递给了price参数,20传递给了no参数
pricenototalPricepriceno
price int,no intprice, no int
func calculateBill(price, no int) int {  var totalPrice = price * noreturn totalPrice
}
functionname(parameters)
package mainimport (  "fmt"
)func calculateBill(price, no int) int {  var totalPrice = price * noreturn totalPrice
}
func main() {  price, no := 90, 6 // 定义 price 和 no,默认类型为 inttotalPrice := calculateBill(price, no)fmt.Println("Total price is", totalPrice) // 打印到控制台上
}

该程序在控制台上打印的结果为

Total price is 540

7.2 多返回值


Go 语言支持一个函数可以有多个返回值。我们来写个以矩形的长和宽为输入参数,计算并返回矩形面积和周长的函数 rectProps。矩形的面积是长度和宽度的乘积, 周长是长度和宽度之和的两倍。即:

  • 面积 = 长 * 宽
  • 周长 = 2 * ( 长 + 宽 )
package mainimport (  "fmt"
)func rectProps(length, width float64)(float64, float64) {  var area = length * widthvar perimeter = (length + width) * 2return area, perimeter
}func main() {  area, perimeter := rectProps(10.8, 5.6)fmt.Printf("Area %f Perimeter %f", area, perimeter) 
}
()func rectProps(length, width float64)(float64, float64)float64lengthwidthfloat64
Area 60.480000 Perimeter 32.800000

7.3 命名返回值


从函数中可以返回一个命名值。一旦命名了返回值,可以认为这些值在函数第一行就被声明为变量了。

上面的 rectProps 函数也可用这个方式写成:

func rectProps(length, width float64) (area, perimeter float64) {  area = length * widthperimeter = (length + width) * 2return // 不需要明确指定返回值,默认返回 area, perimeter 的值
}

请注意:函数中的 return 语句没有显式返回任何值。由于 area 和 perimeter 在函数声明中指定为返回值, 因此当遇到 return 语句时, 它们将自动从函数返回。


7.4 空白符


_
rectProps
rectPropsarea
package mainimport (  "fmt"
)func rectProps(length, width float64) (float64, float64) {  var area = length * widthvar perimeter = (length + width) * 2return area, perimeter
}
func main() {  area, _ := rectProps(10.8, 5.6) // 该函数返回了两个值,但我们只需要第一个值fmt.Printf("Area %f ", area)
}
_ 

7.5 不定长参数


在此之前,我们如果需要向函数传递参数,只能一个值对应一个形参来接收,那如果我们的值非常多,那就需要对应更多的形参来接收,那么使用不定长形参可以接收任意值。

package mainimport "fmt"// 接收任意长度的int类型值
func testIndefiniteLength(numArr ...int) { // 接收到的是一个数字类型的数组,关于数组我们留到后续讲解// numArr[0] 可以取出数组里面的第一个结果fmt.Println(numArr)
}// 我们需要注意的是,不定长参数只能放到位置参数后面,否则程序产生错误
func test(name string, numArr ...int) { fmt.Println(name)fmt.Println(numArr)
}// func test(numArr ...int, name string)
// 这样的程序会产生错误,因为numArr是不定长,程序无法区分哪条数据是要给name参数的func main() {testIndefiniteLength(1, 2, 3, 4, 5)test("jack", 1, 2, 3, 4, 5)// 第一个结果根据位置传递给了name参数,后面多余的结果都传递给了numArr
}

并且一个函数只能存在一个不定长参数,那么如果我们有更多数据类型的参数需要传递呢?这个我们后续可以通过数组来实现。