Loading...
Loading...
Loading...

目录


Golang入门-基础语法

计算机编程 发布于:2022/3/12/13:34 1541 vk python go 最近编辑于2 年,11 月前 预计阅读时长:27min

Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。

Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。

Go 语言特色

  • 简洁、快速、安全
  • 并行、有趣、开源
  • 内存管理、数组安全、编译迅速


Go 语言用途

Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。

对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。

其他语言也可以有高并发,但是go语言的高并发是它天生的
 

学习前提

  1. 具有一种后端语言开发经验(c/c++/python/java/php等)
  2. 具备基本的网络编程能力和并发思想
  3. 熟悉Linux基本命令
  4. 了解计算机基本结构

引用说明

文章内容是我看b站视频https://www.bilibili.com/video/BV1gf4y1r79E?spm_id_from=333.1007.top_right_bar_window_default_collection.content.click作者刘丹冰Aceld,时做的笔记。

 

新的语言

如果你已经掌握了一门语言,那么一定体会到入门一门新的计算机语言无非就时学习它的条件,分支,循环,异常,还有就是这门语言它特有的写法。以下这个go语言的新奇语法

 

看到结构体,指针,main函数这些是不是感觉和c语言有些许相似,所以可以猜到它是一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。编译型就是在运行前先用编译器编译下,才能执行,像如python这种解释型语言可以直接运行,但是代价就是运行效率较低。

下载与安装

这种东西就不再赘述,自行百度,IDE的话可以选择vscode(免费),JetBrains的Goland(理论上收费)但是网上一大堆激活码,或者是学生的自己申请一个学校的学生邮箱激活就行,Linux的话就vim喽。

与其他语言对比

GO语言优劣

优势

大名鼎鼎的docker就是用go开发的。

劣势

Hello,GO!

hello.go


package main
  //程序的包名和程序叫啥名字(hello.go)没关系。

import "fmt"  //引入打印函数所在的包

/*多行注释是这样的,同时引用多个包这么写,末尾无逗号

import (

"fmt"

"time"

)

*/


func main() { 	//注意!这个大括号和函数名一定是同一行的,语法层面的规定否则报错
   fmt.Println("Hello, GO!")  //末尾可以加;但是最好别加
   
}

程序写完后,编译执行的命令:

  • go run hello.go (编译+运行)
  • go build hello.go (生成一个可执行文件)

变量声明

注意!go定义变量的''和""是不一样的定义变量只能用双引号,和python不同。

局部变量声明

  • 方法一:默认值为0  var a int
  • 方法二:自定义初始值 var b int =100
  • 方法三:省略数据类型,自动匹配 var c =100
  • 方法四:省略var关键字,用语法糖: (常用方法)  d:=100

全局变量

方法四不支持

多变量

单行写法

var xx,xy=100,'haha'

多行写法

var(
w int =100
j bool =false

)

常量与枚举

go通过关键字const定义常量。常量顾名思义只读,不可修改。

const a int= 10
const (
a=10
b=20
)

在定义常量的时候有时候会有这种需求比如:希望a=10 b=20 c=30这种,每次都自己手写略显不优雅,可以通过关键字iota来帮助const定义枚举类型

第一行的iota的默认值为0,没增加一行就会累加一

const (
a= iota //iota =0
b     //iota=1
c	  //iota=2
)

//修改表达式可以通过修改首行的表达式来实现各种枚举类型
const(
a= iota *10 //iota=0 0
b			//iota=1 10
c			//iota=2 c 20 
)		
const(
a,b=iota+1,iota +2	//iota=0 1,2
c,d      			//iota=1 2,3
)

函数

返回单个值

package main

import "fmt"

func f1(a string, b int) int {
	fmt.Println(a)
	fmt.Println(b)
	c := 100
	return c
}
func main() {
	c := f1("aaa", 50)
	fmt.Println(c)
}

返回多值匿名

func f2(a string, b int) (int, int) {
	fmt.Println(a)
	fmt.Println(b)

	return 1, 2
}

返回多值带形参

func f3(a string, b int) (r1 int, r2 int) {
	fmt.Println(a)
	fmt.Println(b)
	
	//给带有名称的返回值赋值,不赋值的话默认都为0,r1,r2属于函数的形参为局部变量
	r1 = 100
	r2 = 200
	return
}
//或者
func f3(a string, b int) (r1 int, r2 int) {
	fmt.Println(a)
	fmt.Println(b)
	
	//给带有名称的返回值赋值,不赋值的话默认都为0

	return 100,200
}
//如果r1 r2类型一样可以这么写
func f3(a string, b int) (r1 , r2 int) {
'''

指针

假设我们想写个函数(changeval)来改变变量a的值

package main

import "fmt"

func changeval(p int) {		//这里初始化形参p的时候会开辟一个新的空间,默认值为0,传递参数只是把a的值复制给了p
	p = 10
}
func main() {
	var a int = 1
	changeval(a)	//把a当作参数传入
	fmt.Println(a)		//最后p变成了10但是a依旧是那个少年
}

这时候就要想办法把真正的a传入了,想要传入真正的a那么底层就是传入a的地址就行,只需要设置一个索引让p指向a的地址。

package main

import "fmt"

func changeval(p *int) { //此处定义的p是一个指针类型,指向整型的指针类型
	*p = 10			//*P表示把这个指针指向的地址赋值为10
}
func main() {
	var a int = 1
	changeval(&a)		//&a表示把a的真正地址传入
	fmt.Println(a)
}

练习-两个变量值互换

思路:实现两个变量a,b值互换只需要把b的地址赋值给a,a的地址赋值给b,当然在操作的时候当执行完第一步的时候,两个变量指向的地址就一样了。所以需要定义一个中间变量用来存放第一个变量的值

package main

import "fmt"

func swap(a *int, b *int) {
	var middle int
	middle = *a
	*a = *b
	*b = middle
}
func main() {

	var a int = 10
	var b int = 20
	swap(&a, &b)
	fmt.Println(a, b)
}

当然在python中只需

一行即可,但是实现的 原理还是一样的

defer语句

一个函数在执行最后,结束之前会执行的东西,就是在这个函数生命周期最后执行的东西,类似于django的信号,c++的signal函数,java的finally,通俗一点就是函数在自己快没的时候可以通过defer大叫一声告诉其他人。

从介绍来看,defer在return后执行,因为是生命周期的最后嘛,defer是以压栈的方式执行的,所以多个defer一起的 时候是先进后出的。

package main

import "fmt"

func rt() int {
	fmt.Println("return")
	return 0
}
func df() int {
	defer fmt.Println("1")
	defer fmt.Println("2")
	return rt()
}

func main() {
	df()
}

执行结果长这样

数组

固定长度数组

定义方式

  1. var myarry1 [10] int
  2. myarry2:=[10]  int {1,2,3,4}   //给了十个空间但只定义了前四个,后面的为默认值0
  3. myarry2:=[4]  int {1,2,3,4} 
package main

func printarry(arry [4]int) {	//这里形参只能为[4]因为定义数组的时候长度固定为4了
	//值拷贝
	for index, value := range arry {			\\可以通过range把这个数组的下标和值打印出来
		print("index=", index, "value=", value, "\n")
	}
}

//如果不用index的话这么写:	for _, value := range arry {
//_表示一个匿名参数,意思是你可以不用但必须要有,因为给定的range返回值有两个

func main() {
	myarry1 := [4]int{1, 2, 3, 4}
	printarry(myarry1)
}

如果是定义的固定数组,实在这个printarry函数里修改不了值的,因为它是只拷贝,只是贴了个标签,没有把真正的地址指向数组。这时候就需要动态数组(slice)了,动态数组是引用传递。go中一般默认都是拷贝传递,只有slice.map,channel这些是天生的指针。

动态数组(slice)

定义

所谓动态那么就可以想到它的大小不是固定的了,那么顺理成章。

1.myarry :=[] int {1,2,3,4}   //定义的时候[]里啥都不写不就好,传入了四个值所以分配了四个空间

2.var myarry1 [] int  //声明是一个切片但是没有分配空间

myarry1= make([]int,3) //给myarry1分配了三个空间,初始值为0

3.var myarry2 [] int =make([]int,3)   //声明一个切片同时分配空间

4.myarry3:=make([]int,3)  //声明一个切片同时分配空间,通过:=推到出myarry3是切片

使用

func printarry(arry int) {

//引用传递

}

判断silce是否空切片

if myarry1==nil{	//这个nil约等于其他语言的null但是又有很多不同之处

…
}

else{

‘’'

}

silce追加与截取

追加

之前通过make([]int ,3)这这种方式定义的是指切片的长度,make还可以传一个参数指的是切片的容量。make([]int,4,5)这种意思就是它开辟了5个空间,但是里面只有4个元素

package main

import "fmt"

func main() {
	var numbers = make([]int, 3, 5)
	fmt.Println(len(numbers), cap(numbers)) //长度为3,容量为5
	//追加元素
	numbers = append(numbers, 2)            //第一个参数是要追加的数组,第二个是值
	fmt.Println(len(numbers), cap(numbers)) //长度为4,容量为5
	numbers = append(numbers, 3, 4)		//这里直接追加了两个元素长度为6,numbers的容量不够了,自动开辟原理的二倍的容量,也就是新容量变成10了
	fmt.Println(len(numbers), cap(numbers), numbers) //长度为6,容量为10

}

让我们看一下结果

如果make的时候不传入容量的参数,那么默认和长度一样。

截取

截取很简单和其他语言一样,第一个元素索引为0.

package main

import "fmt"

func main() {
	numbers := []int{1, 2, 3, 4}

	fmt.Println(numbers, numbers[0:2]) //[1,2]

}

map

map用法基本上和silce一样,只不过以键值对存放。

定义

package main

import "fmt"

func main() {
	mymap1 := make(map[int]string)
	mymap1[1] = "go"
	mymap1[2] = "java"
	
	mymap2 := map[int]string{	//当然也可以在定义的时候直接写入一些键值对
		1: "java",		 //末尾记得有逗号
	}
	
	fmt.Println(mymap1)
	fmt.Println(mymap2)

}

使用

package main

import "fmt"

func main() {
	mymap1 := make(map[int]string)
	//添加
	mymap1[1] = "go"
	mymap1[2] = "java"
	//修改
	mymap1[2] = "python"
	//删除
	delete(mymap1, 2)
	//遍历
	for k, v := range mymap1 {
		fmt.Println(k, v)
	}
	fmt.Println(mymap1)

}

!注意map是引用传递,不是拷贝传递,可以直接把map传给一个函数在里面修改即可

包管理

和其他语言的包管理一样,Go modules,如果不用就只能在go语言的环境变量设置的路径下开始写起包的位置,程序会找不到,很长很臭。而且没有版本控制的概念,每次包的版本时都要手撸一遍,很麻烦。这时候包管理工具就该体现作用了,首先要设置各种配置开启go modules。

输入go  env查看当前配置,至于每行配置时什么意思看set后面的英文也可以了解绝大多数了,比如第一行,set GO111MODULE一看就是开启包管理的配置, 怎么进行一些其他个性化设置自行百度

换源等等几乎每个语言都有这种工具,很简单至于怎么开启包管理,这个是它的换源方法,用的七牛云的镜像

go env -w GOPROXY=https://goproxy.cn  

在当前工程环境下使用 go mod init xxx(导入时起的名字)可以初始化一个配置文件:go.mod,命令成功后内部应该出现

module xxx //这个就是你起的名字

go 1.18			//这是go的版本号

导入第三方包

就是从网上下载的包,比如:"github.com/aceld/zinx/znet"


import (
   "fmt"
   "github.com/aceld/zinx/znet"
)

这时候执行go get  github.com/aceld/zinx/znet 

就可以从网上下载下来,再看go.mod你会发现多了一行新的东西

require github.com/aceld/zinx v1.0.1 // indirect

indirect表示间接依赖的意思

与此同时,还多了一个叫go.sum的文件内部长这样

github.com/aceld/zinx v1.0.1 h1:WnahGyE7tDJvkJRVK2VI/m57aHEeUjr12EAYpOYW3ng=
github.com/aceld/zinx v1.0.1/go.mod h1:Tth0Fmjjpel0G8YjCz0jHdL0sXlc4p3Km/l/srvqqKo=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=

就是对远程的包和go.mod本身进行md5校验看看有没有被中间人篡改过保证安全性和完整性。

原理就是:可以自己保存一个文本文件,然后用程序算出它的md5值,然后再改变文本文件的内容,再计算一遍md5值,这时候会发现前后md5值不同了,那么就说明文件被修改了

修改依赖版本

如果已经第三方库的作者把版本更新了,直接go mod get的就是最新版本,go.mod里:require github.com/aceld/zinx v1.0.1 // indirect
可以通过手动把句话的v1.0.1修改成想要的版本,但是不够优雅,因为我们是发生了替换的,这样直接修改日后不太好明白从哪个版本替换成哪个版本的。

要想优雅的实现修改依赖版本,只需要输入

go mod edit -replace v1.0.1=v1.0.2	//(旧版本=新版本)

执行后就会在go.mod文件夹里多处一行结果,最终的go.mod长这样

module tcp_server

go 1.18

require github.com/aceld/zinx v1.0.1 // indirect
replace zinx v1.0.1=>v1.0.2

导入本地包

如果不适用包管理的话,本地路径导包要写绝对路径,或者是go的环境变量那里写开始,或者是放在go语言源码那里,比如导入fmt就i直接写就行,很麻烦。

比如当前的项目目录是这样的

│  go.mod
│  go.sum
│  lib2file.go
│  main.go

└─importlib1
       lib1file.go
       lib1_2.go

 

要想导入importlib1,使用go mod 只需要在main里面输入

import (
	"fmt"
	"tcp_server/importlib1"   //tcp_server 这个是在只需go mod init xxx 的这个xxx,也可以打开go.mod文件手动修改第一行:module tcp_server
 
								//importlib1这个是你向导入包的所在文件夹名字,
)

这里导入的是importlib1这个文件夹,里面的两个文件的第一行必须一样均为package xxx

main文件里面这么调用

	lib1.Libtest()
	lib1.Lib1_2test()

执行结果:

说明了首先是两个int说明了,go在导包的时候初始化包是深度优先的,先把所有要导的包都初始化了再执行其他代码。

相互导入

就是再相同路径下互相导入,比如在main.go中导入lib2file.go。

只需要lib2file.go遵守上面说的规则在同一个文件夹里package xxx必须相同,所以lib2file.go的代码可以写成这样

package main

import "fmt"

func init() {
	fmt.Println("lib2   int")
}
func Lib2test() {
	fmt.Println("lib2 test")
}

想在main.go中使用Libe2test只需要直接执行,无需import 导入

func main() {
	lib1.Libtest()
	lib1.Lib1_2test()
	Lib2test()		//同一路径下的包直接执行,无需导入
}

在编译运行的时候,把两个包同时编译才行,否则只编译一个包那么自然就找不到另一个了

命令行输入go run main.go libe2file.go

成功后就会执行

 


go的基础语法就介绍到这里了。

下一篇是《go入门面向对象》

单词数:693字符数:8178

共有0条评论