Back to Blogs
golang
backend
programming

Golang 基础知识

Soloman
2022-08-02

Golang 基础知识

1 基础概念

以 // 开头的为单行注释,多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾

// hello.go 测试文件
package main

import "fmt"

/* 主函数入口 */
func main() {
	fmt.Println("Hello, www.soloman.vip")
}

执行以上go示例代码,进入代码所在目录执行命令:

# 直接执行go代码
go run hello.go

# 编译生成二进制文件
go build hello.go

1.1 数据类型

数据类型相关描述
布尔型布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true
数字类型整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码
字符串类型字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本
派生类型包括:(a) 指针类型(Pointer)、(b) 数组类型、(c) 结构化类型(struct)、(d) Channel 类型、(e) 函数类型、(f) 切片类型、(g) 接口类型(interface)、(h) Map 类型

1.2 变量

变量名由字母、数字、下划线组成,其中首个字符不能为数字,声明变量常见方式:

// 声明单个变量
var identifier type

// 一次声明多个同类型变量
var identifier1, identifier2 type

// 先声明,再赋值
var v_name v_type
v_name = value

// 直接赋值,自动判断类型
var v_name = value

// 对于新变量(未声明过),同时声明赋值,只能被用在函数体内,而且不可以用于全局变量的声明与赋值
v_name := value

// 类型相同多个变量, 非全局变量
var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3

// 和 python 很像,不需要显示声明类型,自动推断
var vname1, vname2, vname3 = v1, v2, v3

// 出现在 := 左侧的变量不应该是已经被声明过的,否则会导致编译错误
vname1, vname2, vname3 := v1, v2, v3

// 这种因式分解关键字的写法一般用于声明全局变量
var (
    vname1 v_type1
    vname2 v_type2
)

声明变量举例:

package main

var x, y int
var (  // 这种因式分解关键字的写法一般用于声明全局变量
    a int
    b bool
)

var c, d int = 1, 2
var e, f = 123, "hello"

func main(){
    // 这种不带声明格式的只能被用在函数体内,而且不可以用于全局变量的声明与赋值。
    g, h := 123, "hello"
    println(x, y, a, b, c, d, e, f, g, h)
}

值类型

所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值,值类型变量的值存储在堆中

引用类型

复杂数据一般使用引用类型保存,一个引用类型的变量存储的是其值所在的内存地址(数字),或内存地址中第一个字节所在的位置。

注意事项

如果你声明了一个局部变量却没有在相同的代码块中使用它,会得到编译错误。空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。_ 实际上是一个只写变量,你不能获取它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。

变量类型

Go 语言中变量可以在三个地方声明:

  • 函数内定义的变量称为局部变量,作用域只在函数体内,参数和返回值变量也是局部变量
  • 函数外定义的变量称为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用
  • 函数定义中的变量称为形式参数,作为函数的局部变量来使用

1.3 常量

常量中的数据类型只可以是布尔、数字(整数型、浮点型和复数)和字符串。

// 单个常量声明
const identifier [type] = value

// 多个常量声明赋值
const c_name1, c_name2 = value1, value2

常量声明举例

package main

import "fmt"

func main() {
   const LENGTH int = 10
   const WIDTH int = 5  
   var area int
   const a, b, c = 1, false, "str" // 多重赋值

   area = LENGTH * WIDTH
   fmt.Printf("面积为 : %d", area)
   println()
   println(a, b, c)  
}

常量表达式中,函数必须是内置函数,否则编译报错。常量还可以用作枚举:

const (
    Unknown = 0
    Female = 1
    Male = 2
)

iota

iota 在 const 关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)

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

// 等同于
const (
    a = iota
    b
    c
)

iota 只是在同一个 const 常量组内递增,每当有新的 const 关键字时,iota 计数会重新开始。

package main

const (
    i = iota
    j = iota
    k = iota
)
const xx = iota
const yy = iota
func main(){
    println(i, j, k, xx, yy)
}

// 输出是 0 1 2 0 0

2 运算符

Go 语言内置的运算符有:

  • 算术运算符
  • 关系运算符
  • 逻辑运算符
  • 位运算符
  • 赋值运算符
  • 其他运算符

2.1 算术运算符

A=10,B=20

运算符描述实例
+相加A + B 输出结果 30
-相减A - B 输出结果 -10
*相乘A * B 输出结果 200
/相除B / A 输出结果 2
%求余B % A 输出结果 0
++自增A++ 输出结果 11
--自减A-- 输出结果 9

2.2 关系运算符

A=10,B=20

运算符描述实例
==检查两个值是否相等,如果相等返回 True 否则返回 False。(A == B) 为 False
!=检查两个值是否不相等,如果不相等返回 True 否则返回 False。(A != B) 为 True
>检查左边值是否大于右边值,如果是返回 True 否则返回 False。(A > B) 为 False
<检查左边值是否小于右边值,如果是返回 True 否则返回 False。(A < B) 为 True
>=检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。(A >= B) 为 False
<=检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。(A <= B) 为 True

2.3 逻辑运算符

A=true,B=false

运算符描述实例
&&逻辑 AND 运算符。如果两边的操作数都是 True,则条件 True,否则为 False。(A && B) 为 False
||逻辑 OR 运算符。如果两边的操作数有一个 True,则条件 True,否则为 False。(A || B) 为 True
!逻辑 NOT 运算符。如果条件为 True,则逻辑 NOT 条件 False,否则为 True。!(A && B) 为 True

2.4 位运算符

A = 60 = 0011 1100,B = 13 = 0000 1101

运算符描述实例
&按位与运算符。其功能是参与运算的两数各对应的二进位相与。(A & B) 结果为 12, 二进制为 0000 1100
|按位或运算符。其功能是参与运算的两数各对应的二进位相或(A | B) 结果为 61, 二进制为 0011 1101
^按位异或运算符。其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。(A ^ B) 结果为 49, 二进制为 0011 0001
<<左移运算符。左移n位就是乘以2的n次方。其功能把左边的运算数的各二进位全部左移若干位,由右边的数指定移动的位数,高位丢弃,低位补0。A << 2 结果为 240,二进制为 1111 0000
>>右移运算符。右移n位就是除以2的n次方。其功能是把左边的运算数的各二进位全部右移若干位,由右边的数指定移动的位数。A >> 2 结果为 15,二进制为 0000 1111

2.5 赋值运算符

运算符描述实例
=简单的赋值运算符,将一个表达式的值赋给一个左值C = A + B 将 A + B 表达式结果赋值给 C
+=相加后再赋值C += A 等于 C = C + A
-=相减后再赋值C -= A 等于 C = C - A
*=相乘后再赋值C *= A 等于 C = C * A
/=相除后再赋值C /= A 等于 C = C / A
%=求余后再赋值C %= A 等于 C = C % A
<<=左移后赋值C <<= 2 等于 C = C << 2
>>=右移后赋值C >>= 2 等于 C = C >> 2
&=按位与后赋值C &= 2 等于 C = C & 2
^=按位异或后赋值C ^= 2 等于 C = C ^ 2
|=按位或后赋值C |= 2 等于 C = C | 2

2.6 其他运算符

运算符描述实例
&返回变量存储地址&a 将给出变量的实际地址
*指针变量。*a 是一个指针变量

3 条件判断

3.1 if

if 布尔表达式 {
    /* 在布尔表达式为 true 时执行 */
}

if statement; condition {
    /* 在布尔表达式为 true 时执行 */
}

if 布尔表达式 {
    /* 在布尔表达式为 true 时执行 */
} else {
    /* 在布尔表达式为 false 时执行 */
}

// 条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内
if num := 10; num % 2 == 0 { // 判断数字是否为偶数
    fmt.Println(num, "偶数") 
} else {
    fmt.Println(num, "奇数")
}

// if ... else if ... else ...
package main
import "fmt"

func main() {
	var age int
	fmt.Println("Please input your age:")
	fmt.Scan(&age)
	if age < 60 {
		fmt.Println("Too young too naive.")
	} else if age < 90 {
		fmt.Println("Not naive not bold.")
	} else {
		fmt.Println("Too old too bold.")
	}
}

3.2 switch

switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case。使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果。

switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

package main
import "fmt"

func main() {
   /* 定义局部变量 */
   var grade string = "D"
   var marks int = 90

   switch marks {
      case 90: grade = "A"
      case 80: grade = "B"
      case 50,60,70 : grade = "C"
      default: grade = "D"  
   }

   switch {
      case grade == "A" :
         fmt.Printf("优秀!\n" )    
      case grade == "B", grade == "C" :
         fmt.Printf("良好\n" )      
      case grade == "D" :
         fmt.Printf("及格\n" )      
      case grade == "F":
         fmt.Printf("不及格\n" )
      default:
         fmt.Printf("差\n" );
   }
   fmt.Printf("你的等级是 %s\n", grade );      
}

3.3 select

每个 case 必须是一个通信操作,要么是发送要么是接收。select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s);
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}

4 循环

/*
常规for循环
init: 为赋值表达式,给控制变量赋初值;
condition: 关系表达式或逻辑表达式,循环控制条件;
increment: 一般为赋值表达式,给控制变量增量或减量。
*/
for init; condition; increment { }

// 类似while循环
for condition { }

// 死循环
for { }

// for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环
for key, value := range oldMap {
    newMap[key] = value
}

// 示例
package main
import "fmt"

func main() {
	var num int = 0
	for i:=1; i<=10; i++ {
		num += i
		if i == 1 {
			fmt.Printf("Sum of 1 is:%d\n", num)
		} else {
			fmt.Printf("Sum of 1-%d is:%d\n", i, num)
		}
	}
}

// 9 X 9乘法表
func nineNine() {
	const BLANK = "    "
	for i:=1; i<=9; i++ {
		for j:=1; j<=i; j++ {
			fmt.Printf("%d x %d = %d%s", i, j, i*j, BLANK)
		}
		println("")
	}
}

4.1 循环控制语句

控制语句描述
break经常用于中断当前 for 循环或跳出 switch 语句
continue跳过当前循环的剩余语句,然后继续进行下一轮循环
goto将控制转移到被标记的语句

5 函数

// 函数定义
func function_name( [parameter list] ) [return_types] {
    函数体
}

// 值传递
func swap(x, y int) int {
   var temp int
   temp = x /* 保存 x 的值 */
   x = y    /* 将 y 值赋给 x */
   y = temp /* 将 temp 值赋给 y*/
   return temp;
}

// 引用传递
func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保存 x 地址上的值 */
   *x = *y      /* 将 y 值赋给 x */
   *y = temp    /* 将 temp 值赋给 y */
}

5.1 函数参数传递

传递类型描述
值传递值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。go函数默认即为值传递
引用传递引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

6 数组

Go 数组可以存储同一类型的数据,声明数组需要指定元素类型及元素个数,如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度。初始化数组中 { } 中的元素个数不能大于 [ ] 中的数字。

// 一维数组
var arrayName [SIZE] variable_type

// m * n 二维数组
var arrayName [ m ][ n ] variable_type

// 多维数组
var arrayName [SIZE1][SIZE2]...[SIZEN] variable_type

// 示例
func arrayTest() {
	var arr = [2][3] string {
		{"www.", "soloman.vip", "/zh"},
		{"www.", "sunnie.fun", "/kr"},
	}
	for i, row := range arr {
		for j, val := range row {
			fmt.Printf("arr[%d][%d]=%s\t", i, j, val)
		}
		println("")
	}
}

6.1 区分数组与切片

Go 数组是值,其长度是其类型的一部分,作为函数参数时,是值传递,函数中的修改对外部无影响。[ ] 不指定长度参数时表示是切片类型,Go 切片包含对底层数组内容的引用,作为函数参数时,类似于指针传递,函数中的修改对外部参数有影响。

7 指针

一个指针变量指向了一个值的内存地址。Go 语言的取地址符是 &,放到一个变量前就会返回相应变量的内存地址。在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。

var var_name *var-type

// 声明指针数组
var ptr [MAX]*int;

// 指向指针的指针
var ptr **int;

// 示例
func pointerTest() {
	var a int = 10
	var ptr *int
	var pptr **int
	ptr = &a
	pptr = &ptr
	fmt.Printf("Value of variable a is %d\n", a)
	fmt.Printf("Address of variable a is %x\n", ptr)
	fmt.Printf("Address of pointer ptr is %x\n", pptr)
}

当一个指针被定义后没有分配到任何变量时,它的值为 nil。nil 指针也称为空指针。nil 在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。

8 结构体

结构体中属性的首字母大写相当于 public,首字母小写相当于 private。这个 public 和 private 是相对于包(go 文件首行的 package 后面跟的包名)来说的。

type struct_variable_type struct {
   member definition
   member definition
   ...
   member definition
}

// 定义结构体指针
var struct_pointer *struct_variable_type

struct_pointer = &struct_variable

// 示例
func structTest() {
	type Websites struct {
		name string
		domain string
		category string
		age int
	}
	var soloman = Websites{"Soloman", "soloman.vip", "personal site", 6}
	var psite *Websites
	psite = &soloman
	fmt.Printf("Site %s is a %s, \nwhose domain is %s, \nwhich was built %d year ago.",
		psite.name, psite.category, psite.domain, psite.age,
		)
}

9 切片

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大(类似Python的List)。一个切片在未初始化时为 nil,长度为 0。

// 切片无需指定长度
var identifier []type

// 通过make()函数创建切片,这里 len 是数组的长度并且也是切片的初始长度
var slice_name []type = make([]type, len)
slice_name := make([]type, len)

// 指定容量
make([]type, length, capacity)

nums1 := []int {0, 1, 2, 3}
var num2 = make([]int, 3, 5)
var num3 []int

10 字典

Map 是一种无序的键值对的集合,底层使用 hash 表来实现。

/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)

countryCapital := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}

11 接口

接口把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

/* 定义接口 */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
   /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法实现*/
}

// 示例
package main
import (
    "fmt"
)

type Phone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = new(NokiaPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()

}

12 并发和通道

Go 语言支持并发,通过 go 关键字来开启 goroutine 即可。goroutine 是轻量级线程,goroutine 的调度由 Golang 运行时进行管理。同一个程序中的所有 goroutine 共享同一个地址空间。

go 函数名( 参数列表 )

go function(x, y, z)

// 示例
package main

import(
	"fmt"
	"time"
)


func main() {
	go saying("Hello")
	go saying("Welcome to www.soloman.vip")
	saying("Nice to meet you")
}

func saying(sentence string) {
	for i:=0; i<5; i++ {
		time.Sleep(1000 * time.Millisecond)
		fmt.Println(sentence)
	}
}

12.1 通道

通道(channel)是用来传递数据的一个数据结构。通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

ch := make(chan int)

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据,并把值赋给 v

// 创建带缓冲区的通道
ch := make(chan int, 100)

13 参考文档

1.Go Google 官方下载

2.Golang 官方网站