Go语言基础语法讲解与学习

Go语言基础语法讲解与学习

1 Go语言基础知识介绍

​ 介绍Go语言之前,我们先了解一下有哪些开源项目是Go语言开发的,其中就包括 Docker、Go-Ethereum、Thrraform 和 Kubernetes。

​ Go语言经过了十几年的发展与成长,已经得到了不断的完善和优化。所以对于像我们这样想学习goalng的入门初学者来说,只要保持正确的学习方向和路径,我们的学习成果一定能够得到证明我们的努力和付出是值得的!

​ 另外说下学习golang的一些个人感受:golang在某些地方很像C语言,所以如果你已经掌握了C语言或其它编程语言,那么你能够很快的掌握golang的一些基础运用。并且你会在对golang的一些指针操作、内存分配以及多协程处理上有更深的认识和体会。但是在日后的项目设计上,建议可以先尝试以golang的设计思想去实现一些功能设计,而不要代入C的一些设计思想。如果你没有学习过C语言或者其他编程语言,golang作为你的第一入门语言来说也不会非常困难,因为它的语法和规则以及设计思想已经帮助你解决了很多问题,而你只需要把大部分精力用来进行代码设计。但是,请不要放弃对底层原理的学习和探索,这样你才能够对golang掌握的更加熟练。

1.1 Go语言设计的由来

​ Go语言出自 Ken Thompson 和 Rob Pike、Robert Griesemer 之手,他们都是计算机科学领域的重量级人物。

  • Ken Thompson

    贝尔实验室 Unix 团队成员,C语言、Unix 和 Plan 9 的创始人之一,在 20 世纪 70 年代,设计并实现了最初的 UNIX 操作系统,仅从这一点说,他对计算机科学的贡献怎么强调都不过分。他还与 Rob Pike 合作设计了 UTF-8 编码方案。

  • Rob Pike

    Go语言项目总负责人,贝尔实验室 Unix 团队成员,除帮助设计 UTF-8 外,还帮助开发了分布式多用户操作系统 Plan 9、Inferno 操作系统和 Limbo 编程语言,并与人合著了《The Unix Programming Environment》,对 UNIX 的设计理念做了正统的阐述。

  • Robert Griesemer

    就职于 Google,参与开发 Java HotSpot 虚拟机,对语言设计有深入的认识,并负责 Chrome 浏览器和 Node.js 使用的 Google V8 JavaScript 引擎的代码生成部分。

​ 其实Golang的诞生初衷是为了满足Google本身的需求,但是这门语言的设计却融合了Go语言团队多年的经验和对编程语言设计的深入认识。设计团队借鉴了 Pascal、Oberon 和C语言的设计智慧,同时让Go语言具备动态语言的便利性。

​ Go语言的所有设计者都说,设计Go语言是因为 C++ 给他们带来了挫败感。在 Google I/O 2012 的 Go 设计小组见面会上,Rob Pike 是这样说的:

我们做了大量的 C++ 开发,厌烦了等待编译完成,尽管这是玩笑,但在很大程度上来说也是事实。

1.2 Go 是编译型语言

​ Go 使用编译器来编译代码。编译器将源代码编译成二进制(或字节码)格式;在编译代码时,编译器检查错误、优化性能并输出可在不同平台上运行的二进制文件。要创建并运行 Go 程序,程序员必须执行如下步骤。

(1)使用文本编辑器创建 Go 程序;

(2)保存文件;

(3)编译程序;

(4)运行编译得到的可执行文件。

​ 这不同于 Python、Ruby 和 JavaScript 等语言,它们不包含编译步骤。Go 自带了编译器,因此无须单独安装编译器。

1.3 Go工程结构

一个Go语言项目的目录一般包含以下三个子目录:

  • src 目录:放置项目和库的源文件;
  • pkg 目录:放置编译后生成的包/库的归档文件;
  • bin 目录:放置编译后生成的可执行文件。

三个目录中我们需要重点关注的是 src 目录,其他两个目录了解即可,下面来分别介绍一下这三个目录。

src 目录

​ 用于以包(package)的形式组织并存放 Go 源文件,这里的包与 src 下的每个子目录是一一对应。例如,若一个源文件被声明属于 log 包,那么它就应当保存在 src/log 目录中。

​ 并不是说 src 目录下不能存放 Go 源文件,一般在测试或演示的时候也可以把 Go 源文件直接放在 src 目录下,但是这么做的话就只能声明该源文件属于 main 包了。正常开发中还是建议大家把 Go 源文件放入特定的目录中。

​ 包是Go语言管理代码的重要机制,其作用类似于Java中的 package 和 C/C++ 的头文件。Go 源文件中第一段有效代码必须是package <包名> 的形式,如 package hello。

​ 另外需要注意的是,Go语言会把通过go get 命令获取到的库源文件下载到 src 目录下对应的文件夹当中。

pkg 目录

​ 用于存放通过go install 命令安装某个包后的归档文件。归档文件是指那些名称以“.a”结尾的文件。

​ 该目录与 GOROOT 目录(也就是Go语言的安装目录)下的 pkg 目录功能类似,区别在于这里的 pkg 目录专门用来存放项目代码的归档文件。

​ 编译和安装项目代码的过程一般会以代码包为单位进行,比如 log 包被编译安装后,将生成一个名为 log.a 的归档文件,并存放在当前项目的 pkg 目录下。

bin 目录

​ 与 pkg 目录类似,在通过go install 命令完成安装后,保存由 Go 命令源文件生成的可执行文件。在类 Unix 操作系统下,这个可执行文件的名称与命令源文件的文件名相同。而在 Windows 操作系统下,这个可执行文件的名称则是命令源文件的文件名加 .exe 后缀。

2 Go基本语法

2.1 变量

​ 数学课中我们知道变量表示没有固定值且可改变的数,计算机课中我们又了解到变量是一段或多段用来存储数据的内存。

​ 作为静态类型的Go语言来说,Go的变量总是有固定的数据类型,类型决定了变量内存的长度和存储格式。我们可以修改变量的变量值,但是无法更改变量的变量类型。

  • 定义

    关键字var用来定义变量,类型放在变量名的后面,例如:

    var x int			// go会在定义的时候自动初始化变量,int类型默认为0
    var y = true		// 也可在定义的时候进行赋值,这里会自动推断出bool类型,并赋值ture
    var a,b,c string	// 同时定义多个相同类型的变量
    var i,str = 1, "string"	// 同时定义并初始化多个不同类型的变量
    
    // 一般会在多个变量需要定义时采用组的形式进行定义
    var (
    	x int
        y = true
        a,b,c string
        i,str = 1, "string"
    )
    

    ​ 上面是一般的定义形式,我们可以放在开头进行变量定义。除了上面的定义方式外,golang还提供了一种简短模式进行变量定义,例如:

    func main() {
        y := true
        i, str = 1, "string"
    }
    

    ​ 这种定义方式通过:=来进行定义,但是使用的时候一定需要主要以下几点:

    (1)定义变量同时显式初始化。

    (2)不能提供数据类型。

    (3)只能在函数内部使用

    ​ 我们在使用上面这种定义方式的时候一定需要弄清楚变量的作用域,因为有时候变量名称相同,但是表示的作用域和含义是不同的。例如:

    var gX = 100		// 先定义一个int类型的全局变量gX,值是100
    
    func main () {
        fmt.Println(&gX, gX)	// 打印gX变量的地址和值
        
        gX := "str"
        
        fmt.Println(&gX, gX)
    }
    
    /*
    输出:
    0xac041			100
    0xc630040a10	"str"
    
    对比内存地址和值就可以发现上面两个gX变量虽然名字相同,但是却是两个不同的变量。
    */
    

    ​ golang里面还有一种匿名变量,当我们遇到一些没有名称的变量、类型或者方法的时候就能使用匿名变量来增强代码的灵活性。

    ​ 匿名变量的特点就是一个下划线__本身就是一个特殊的标识符,可以称作空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。例如:

    func getNumber()(int,int){
        return 1, 100
    }
    
    func main() {
        num1, _ := getNumber()
        _, num100 := getNumber()
        
        fmt.Println(num1, num100)
    }
    
    /*
    输出:
    1 100
    
    这里的getNumber函数是拥有两个整数的返回值,所以每次调用的时候会返回1和100两个数值。
    当我们只想使用其中某一个数值时,我们就可以使用匿名变量来接收其中一个数值。
    */
    

    注意:匿名变量不占用内存空间,也不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用

  • 作用域

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

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

    我们可以根据变量的位置不同,分为局部变量全局变量形式参数三个类型。

    • 局部变量

      声明在函数体内的变量我们称之为局部变量,它的作用域只在函数体内部,函数的参数和返回值变量都属于局部变量。了解C知识就知道,局部变量不是一直存在的,它只定义在它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁掉。例如:

      package main
      
      import (
      	"fmt"
      )
      
      // main函数体外部
      
      func main() {
          // main函数体内部
      	
          // 我们在main函数体内部声明两个变量x,y并赋值
          x := 0
          y := 1
          
          // 声明一个num变量并将x和y的和赋值给num
          num := x + y
          
          fmt.Printf(" x = ",x," y = ",y," num = ",num)
      }
      
      /*
      输出:
       x = 0 y = 1 num = 1
       
      这里面的x,y,num三个变量都属于局部变量
      */
      
    • 全局变量

      ​ 在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,当然,不包含这个全局变量的源文件需要使用“import”关键字引入全局变量所在的源文件之后才能使用这个全局变量。

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

      package main
      
      import (
      	"fmt"
      )
      
      // main函数体外部
      // 声明一个全局变量num
      var num int
      
      func main() {
          // main函数体内部
      	
          // 我们在main函数体内部声明两个变量x,y并赋值
          x := 0
          y := 1
          
          // 给全局num变量进行赋值
          num = x + y
          
          fmt.Printf(" x = ",x," y = ",y," num = ",num)
      }
      
      /*
      输出:
       x = 0 y = 1 num = 1
       
      这里面的x,y两个变量属于局部变量,num属于全局变量
      */
      
      

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

    • 形式参数

      ​ 在定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。

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

      package main
      
      import (
          "fmt"
      )
      
      //全局变量 a
      var a int = 33
      func main() {
          //局部变量 a 和 b
          var a int = 1
          var b int = 0
          fmt.Printf("main() 函数中 a = %d\n", a)
          fmt.Printf("main() 函数中 b = %d\n", b)
          c := sum(a, b)
          fmt.Printf("main() 函数中 c = %d\n", c)
      }
      func sum(a, b int) int {
          fmt.Printf("sum() 函数中 a = %d\n", a)
          fmt.Printf("sum() 函数中 b = %d\n", b)
          num := a + b
          return num
      }
      
      /*
      输出:
      main() 函数中 a = 1
      main() 函数中 b = 0
      sum() 函数中 a = 1
      sum() 函数中 b = 0
      main() 函数中 c = 1
      */
      

2.2 常量

​ 常量表示运行时恒定并且不可改变的值,通常是一些字面量。Go语言中使用关键字const来定义常量,用来存储这些不会改变的数据。

​ 常量值必须是在编译期就可以确定的字符、字符串、数字或布尔值。例如:

// 显式类型定义: 
const a string = "Hello"

// 隐式类型定义的时候编译器可以根据变量值来推断其类型
// 隐式类型定义:
const a =  "Hello"

// 常量可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。
正确做法:
const b = 1+1

错误做法:
const b = getNumber()

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

  • iota常量生成器

    Go语言并没有明确意义上的enum枚举定义,不过我们可以借助iota标识符来实现一组自增常量值。例如:

    const (
    	x = iota // 0
    	y		 // 1
    	z  	 	 // 2const (
    	_  = iota // 0
        KB = 1 << (10 * iota)   // 1 << (10 * 1)
        MB 						// 1 << (10 * 2)
        GB 						// 1 << (10 * 3)
    )
    

    我们也可以在多个常量定义中使用多个iota,它们各自单独计数,但是需要确保每行常量的列数相同。例如:

    const (
    	_, _ = iota,iota*10		//0, 0*10
        a, b					//1, 1*10
        c, d					//2, 2*10
    )
    

    我们如果希望中断iota的自增,我们就需要显式恢复。同时如果后续又想使用iota,则后续自增是按照行序递增的,不像C语言的enum那样按上一取值递增。例如:

    const (
    	a = iota		// 0
    	b				// 1
    	c = 100			// 100
    	d				// 100(iota自增中断,值同上一行常量右值表达式)
    	e = iota		//	4(恢复iota自增,值按照行序递增,包括c、d)
    	f				// 5

2.3 命名

​ 这里给一些命名建议:

  • Go的命名区分大小写,并且首字母大小写决定了其作用域。
  • 一般使用驼峰命名法。
  • 局部变量优先使用短名称。
  • 命名一般是由字母或者下划线开始,由多个字母、数字和下划线组合而成。
  • 不要使用保留关键字。
  • 不建议使用与预定义常量、类型、内置函数相同的名字。
  • 专有名词一般全部大写。

2.4 基本类型

类型长度(字节)默认值说明
bool1false布尔类型,真用true表示,假用false表示
byte10字节类型,可以看作是一个8位二进制数表示的无符号整数类型,uint8
int,uint4,80默认整数类型,根据平台32位或64位不同,长度也不同
int8,uint810-128~127,0~255
int16,uint1620-32768~32767,0~65535
int32,uint3240-21亿~21亿,0~42亿
int64,uint6480
float3240.0由32位二进制数表示的浮点数类型
float6480.0由64位二进制数表示的浮点数类型,默认浮点数类型
complex6480.0+0.01由64位二进制数表示的复数类型,float32类型的实部和虚部联合表示
complex128160.0+.0.01由128位二进制数表示的复数类型,float64类型的实部和虚部联合表示
rune40unicode code point, int32
uintptr4,80足以存储指针的uint
string-“”字符串,默认为空字符串,而非NULL
array数组
struct结构体
functionnil函数
interfacenil接口
mapnil字典,引用类型
slicenil切片,引用类型
channelnil通道,引用类型
  • 引用类型

    ​ 引用类型特指slicemapchannel这三种预定义类型。

    ​ 引用类型拥有更复杂的存储结构,除了分配内存以外,它们还须初始化一系列的属性,例如:指针、长度,甚至包含哈希分布、数据队列等。

    ​ 内置函数new可以按照指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型必须使用make函数创建,编译器会将make转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关属性初始化。

  • 整数类型

    ​ Go语言的数值类型分为以下几种:整数、浮点数、复数,其中每一种都包含了不同大小的数值类型,例如有符号整数包含 int8int16int32int64 等,每种数值类型都决定了对应的大小范围和是否支持正负符号。

    ​ 大多数情况下,我们只需要 int 一种整型即可,它可以用于循环计数器(for 循环中控制循环次数的变量)、数组和切片的索引,以及任何通用目的的整型运算符,通常 int 类型的处理速度也是最快的。

  • 浮点数类型

    ​ Go语言提供了两种精度的浮点数 float32float64,它们的算术规范由 IEEE754 浮点数国际标准定义,该浮点数规范被所有现代的 CPU 支持。

    ​ 一个 float32 类型的浮点数可以提供大约 6 个十进制数的精度,而 float64 则可以提供约 15 个十进制数的精度,通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大。

  • 复数类型

    ​ Go语言中复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型。

    ​ 复数的值由三部分组成 RE + IMi,其中 RE 是实数部分,IM 是虚数部分,REIM 均为 float 类型,而最后的 i 是虚数单位。

    ​ 对于一个复数z := complex(x, y),可以通过Go语言的内置函数real(z) 来获得该复数的实部,也就是 x;通过imag(z) 获得该复数的虚部,也就是 y。

  • 布尔类型

    ​ 一个布尔类型的值只有两种:true false。if 和 for 语句的条件部分都是布尔类型的值,并且==<等比较操作也会产生布尔型的值。

    ​ 布尔型无法参与数值运算,也无法与其他类型进行转换。

  • 字符串类型

    ​ 字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。

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

    • \n:换行符
    • \r:回车符
    • \t:tab 键
    • \u\U:Unicode 字符
    • \\:反斜杠自身
  • 字符类型

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

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

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

    ​ Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明:valueOfTypeB = typeB(valueOfTypeA)

    ​ 只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int类型)。

2.5 关键字

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

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

breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

​ 之所以刻意地将Go语言中的关键字保持的这么少,是为了简化在编译过程中的代码解析。和其它语言一样,关键字不能够作标识符使用。

  • 标识符

    ​ 标识符是指Go语言对各种变量、方法、函数等命名时使用的字符序列,标识符由若干个字母、下划线_、和数字组成,且第一个字符必须是字母。通俗的讲就是凡可以自己定义的名称都可以叫做标识符。

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

    • 由 26 个英文字母、0~9、_组成;

    • 不能以数字开头,例如 var 1num int 是错误的;

    • Go语言中严格区分大小写;

    • 标识符不能包含空格;

    • 不能以系统保留关键字作为标识符,比如 break,if 等等。

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

    • 标识符的命名要尽量采取简短且有意义;

    • 不能和标准库中的包名重复;

    • 为变量、函数、常量命名时采用驼峰命名法,例如 stuNamegetVal

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

appendboolbytecapclosecomplexcomplex64complex128uint16
copyfalsefloat32float64imagintint8int16uint32
int32int64iotalenmakenewnilpanicuint64
printprintlnrealrecoverstringtrueuintuint8uintptr

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

2.6 运算符

​ 运算符是用来在程序运行时执行数学或逻辑运算的,在Go语言中,一个表达式可以包含多个运算符,当表达式中存在多个运算符时,就会遇到优先级的问题,此时应该先处理哪个运算符呢?这个就由Go语言运算符的优先级来决定的。

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

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

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

2.7 小结

​ 掌握基本的语法知识是学习一门语言的必经之路,基础的知识只要细细思考和琢磨就会引发出多种多样的思路和问题。我们探索和解决这些问题的过程就是在巩固基础与深入学习,基础打得牢才能事半功倍。

​ 本文参考并借鉴了雨痕大佬的《Go语言学习笔记》,有兴趣的可以去看看这本书。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值