Go,又称Golang,是google开源的编程语言,原生支持并发编程

  • 没有对象的概念,不支持继承、多态,没有泛型、try…catch
  • 有接口、函数式编程、csp并发模型(goroutine+channel)

 

.go文件基本结构

/*
一个包可以对应多个目录,一个目录只能对应一个包,同一目录下包名需要保持一致
*/
package main


/*
如果导入的包依赖其它包,会先导入其它包
导入包时,会先初始化包中的公共常量、变量,如果有init()方法,会自动执行init()方法
*/
import (
	"fmt"
	complex "math/cmplx" //定义别名,可通过别名来引用
)


/*
同一包中,不能有同名的公共常量、变量、自定义的方法名,只能有一个main()方法
 */
const name = "chy"
var age = 1


func init() {
	fmt.Println("执行init()...")
	fmt.Println(name)
	fmt.Println(age)
}


/*
执行入口是main包下的main()方法
执行main时,会先导入依赖的包,再初始化常量、变量,如果有init()还会执行init(),最后执行main()
*/
func main() {
	fmt.Println("执行main()...")
	complex.Abs(1 + 1i)
}

 

变量

//go是先写变量名,再写数据类型
var name1 string = "chy"
//可以缺省类型,会自动推断类型
var name2 = "chy"


//可以同时声明多个变量,进行多个变量的赋值
var name3, age3 = "chy", 20
name3, age3 = "chy1", 21

//可以指定数据类型,这个数据类型是给前面所有的变量指定的
//var age4, age5 int = 20, 20

//也可以这样写
var (
	name6 = "chy"
	age6 = 20
)
func f1() {
	//函数外可以定义未被使用的变量,函数内不能随便定义变量,定义了变量就必须使用
	var age1 = 20

	//直接用 = 赋值不能算是使用
	age1 += 1

	//在函数内,可以用 := 表示声明并赋值,相当于冒号相当于var
	name1 := "chy"
	name2, age2 := "chy", 20

	fmt.Println(name1, name2, age2)
}
  • 函数的作用域是当前包中,函数外声明的变量作用域也是当前包中
  • 函数内声明的变量的作用域是局部变量,作用域是当前函数内

 

常量

  • golang中常|变量名的大小写有特殊含义,常量名一般不全大写
  • golang没有专门的枚举类型,可以用常量实现类似的功能
  • iota是一个内置的自然数递增器,需要和const搭配使用
//常量的用法和变量差不多,但常量声明时就需要赋值,且后续不能修改值;变量可以先声明再赋值,但声明时必须指定数据类型
var name1 string
const name1 = "chy"


//如果要定义多个常量,更推荐这种写法
const (
	name2 = "xxx"
	port2 = 9000
)
//自增型常量,0,1,2... 可以用_跳过当前值
const (
	java = iota
	_
	python
	golang
)


//支持表达式,1,3,5...
const (
	a = iota*2 + 1
	b
	c
)


//1(2^0),1024(2^10),1024*1024(2^20)...
const (
	byte = 1 << (10 * iota)
	kb
	mb
	gb
)

 

数据类型

内建数据类型

  • (u)int8、(u)int16、(u)int32、(u)int64、(u)int:go没有short、int、long之类的整型,以数字标识位数,/8即是该类型的字节数,以对应不同长度的整型。(u)int由操作系统位数决定,32位操作系统分配32位,64位操作系统分配64位。u表示无符号。
  • uintptr 无符号整型,用于存放一个指针
  • float32、float64:分别对应其它语言中的float、double。不用像其它语言一样需要在值末尾加f。
  • complex64、complex128:复数,64是32位实数+32位虚数,128是64位实数+64位虚数。
  • byte:uint8的别名
  • rune:int32的别名,相当于其它语言中的char
  • bool
  • string:工具类strings提供了很多操作字符串的方法
//双引会解析其中的转义字符,反引号则不解析其中的转义字符
var str1, str2 = "hello\nworld\n", `hello\nworld\n`
fmt.Println(str1, str2)


//复数,cmplx库也提供了很多操作复数的函数
var c1, c2 = 1 + 1i, 2 + 2i
fmt.Println(c1 + c2)

已声明但未赋值,也有默认的初始值,整型、浮点型初始值为0,字符串初始值为空串,bool型初始值为false。

 

指针 Pointer

参数传递方式

  • 值传递:把实参的值copy一份传给形参,如果是对象,copy的是对象的引用。java中只有值传递。
  • 指针传递:传入的实参的指针|地址,对形参的操作就是对实参的操作。
  • 引用传递:形参是实参的别名,对形参的操作就是对实参的操作。
//值传递
func f1(a int) {
	a = 1
}


//指针传递。参数的数据类型是*int,在原类型前加*。可以把 *形参变量 当做 实参变量
func f2(pa *int) {
	fmt.Println(*pa)
	*pa = 100
}

//指针传递,交换2个变量的值
func swap(a, b *int) {
	*a, *b = *b, *a
}
//引用传递,此处以c++为例。&a可以理解为 给实参取别名a
void swap(int &a, int &b) {
	int temp = a;
	a = b;
	b= temp;
}

int main() {
	int x = 1;
	int y = 2;
	swap(x, y);
	return 0;
}

 

结构体

//结构体。里面定义字段即可
type node struct {
	value     int
	pre, next *node
}

//自定义的工厂函数,golang允许返回局部变量的地址给外部使用
func createNode(value int) *node {
	return &node{value: value}
}

//调用方式是node.getValue()
func (node node) getValue1() int {
	return node.value
}

//调用方式是getValue2(node)
func getValue2(node node) int {
	return node.value
}

//参数不加*是传值,copy整个实参对象传给形参,对形参的修改不会影响到实参;加*是传地址,对形参的修改就是对实参的修改
func (node *node) setValue1(value int) {
	node.value = value
}

//xxx(*xxx,xxx)调用,*作为参数时,需要&取地址传入
func setValue2(node *node, value int) {
	node.value = value
}


func main() {
	//各字段都会初始化为对应类型的默认值,指针类型的默认初始值是nil
	var head node

	//0
	fmt.Println(head.value)
	//nil
	fmt.Println(head.pre)
	//报错,需要先判断,head.pre != nil 才进行操作
	//fmt.Println(head.pre.value)

	
	//可以用new直接创建,效果和var head node一样,都是初始化各字段为该类型的默认初始值
	head.pre = new(node)
	//可以调用自定义的工厂函数创建
	head.next = createNode(10)

	//也可以node{}创建。node{}和var head node效果差不多,各字段都是使用默认的初始值
	head = node{}
	//可以只给部分字段赋值,但需要指定字段名
	head = node{value: 10}
	//给全部字段赋值时,可以缺省字段名
	head = node{10, nil, nil}


	//可以直接(访问|修改)操作字段。指针类型赋的是地址
	head.value = 0
	head.pre = &node{}
	head.next = &node{100, nil, nil}


	//如果定义函数时:参数是指针(加*),会自动取head的地址(&head)传入;如果参数没有加*,会自动copy整个对象传入
	head.setValue1(111)
	//如果定义函数时:参数是指针(加*),需要&取地址传入;如果参数没有加*,直接传入head即可
	setValue2(&head, 111)

	fmt.Println(head.getValue1())
	fmt.Println(getValue2(head))


	//数组形式
	nodeArr := []node{
		{},
		{value: 1},
		{1, nil, nil},
	}
	fmt.Println(nodeArr)
}
  • golang只支持封装,不支持继承和多态。
  • golang中,局部变量不一定在栈上创建,不一定随着方法调用的结束而销毁。如果局部变量暴露出去给外部使用,这个局部变量会在堆上创建,由gc回收。
  • 如果要修改实参,参数使用指针;如果结构体变量很大,copy开销大,也可以考虑直接传入指针。

 

接口

接口的定义和实现
//定义一个接口,里面包含一些方法
	type UserService interface {
		findUsernameById(id int) string
	}
//定义一个结构体,实现接口中的所有方法
//实现接口中的所有方法,这个结构体就算实现了接口,只实现了部分方法则不算
type UserServiceImpl struct {

}

//方法要写成xxx.xxx()的调用形式
func (userServiceImpl UserServiceImpl) findUsernameById(id int) string {
	return "xxx"
}
func f1(userService UserService, id int) {
	username := userService.findUsernameById(id)
	fmt.Println(username)
}

func main() {
	userServiceImpl := UserServiceImpl{}
	f1(userServiceImpl, 1)
}

 

任何类型

interface{ }表示任意类型,相当于java中的Object

func f2(i interface{}) {
	fmt.Println(i)
}

func main() {
	var a interface{} = 1
	//可以更改值的类型
	a = "xxx"
	f2(a)
}

 

接口的组合

Get接口

type Get interface {
	Get(url string) string
}

type GetImpl struct {
}

func (get GetImpl) Get(url string) string {
	return "this is get response:" + url
}

 
Post接口

type Post interface {
	Post(url string) string
}

type PostImpl struct {
}

func (post PostImpl) Post(url string) string {
	return "this is the post response:" + url
}

 
Http接口

//相当于接口的继承,可以继承其它接口,继承之后可以调用父接口中的方法
type Http interface {
	//可包含一些其它接口,这些接口相当于当前接口的父接口
	Get
	Post
	//可包含一些其它方法
}

type HttpImpl struct {
	//父接口的实现类
	GetImpl
	PostImpl
}

//其它方法实现

 
使用

func f1(http Http) {
	url := "http://www.baidu.com"
	getResp := http.Get(url)
	postResp := http.Post(url)
	fmt.Println(getResp, postResp)
}

func main() {
	http := HttpImpl{}
	f1(http)
}

 

内建容器

数组 Array
//元素不够时,自动填充为该类型的初始值
var arr1 = [5]int{1, 2, 3}

//可以将长度写成... 自动取元素的实际个数
var arr2 = [...]int{1, 2, 3}

//可以先声明、后续赋值,已声明、但未赋值的数组,各元素为该类型的初始值
var arr3 [11]int

//二维数组,5行4列,元素是一维数组,元素不够时自动填充(该类型的初始值的一维数组)
var arr4 = [5][4]int{{}, {1}, {1, 2}, {1, 2, 3}}
func f1(arr [5]int) {
	arr[0] = 100
}

func f2(arr [...]int) {
	arr[0] = 100
}


func main() {
	//arr1和f2的参数类型不匹配,不能传入
	arr1 := [5]int{1, 2, 3}
	f1(arr1)
	f2(arr1)

	//arr2的实际类型是[3]int,和f1、f2的参数类型均不匹配,不能传入
	arr2 := [...]int{1, 2, 3}
	f1(arr2)
	f2(arr2)

	//arr3已声明、未赋值,类型还是[...]int,可以传给f2,不能传给f1
	var arr3 [...]int
	f1(arr3)
	f2(arr3)
}
  • 数组属于值传递,传的并不是数组的地址、引用,而是把整个数组copy一份传给形参,对形参数组的修改不会影响到实参数组。
  • [3]int、[5]int、[…]int是三种数据类型,传入的数据类型要对应。
  • golang中一般不直接使用数组,而是使用切片代替。

 

切片 Slice

切片:从数组或其它切片上切下一个连续片段。

//与数组相比,切片不指定长度
var arr = [...]int{1, 2, 3, 4, 5}
var slice = []int{1, 2, 3, 4, 5}

func main() {
	//切片的数据可以来源于数组,也可以来源于另一个切片,此处是来源于另一个切片

	//[1,3)
	var slice1 = slice[1:3]
	//[0,3)
	var slice2 = slice[:3]
	//到末尾
	var slice3 = slice[1:]
	//全部元素
	var slice4 = slice[:]

	//不管来源于数组、还是另一个切片,切片的底层数据结构都是数组
	//切片只是底层数组的一个视图,对切片的修改实际是对底层数组的修改,对底层数组的修改也会同步到对应的视图(切片)上
	slice[0] = 100
	slice1[1] = 1000
	fmt.Println(slice1, slice2, slice3, slice4)
}

无论是数组还是切片,golang都不支持负数索引。

 

slice的组成

  • ptr:slice中第一个元素的指针
  • len:slice的长度(实际元素个数)
  • cap:可用容量,ptr到底层数组末尾都属于可用范围,slice扩展时只能向后扩展,使用底层数组中的位置,数组本身不可扩展,如果底层数组容量不够,会自动扩容——声明一个容量更大的数组,将原数组的元素复制到新数组中。
    在这里插入图片描述

slice的扩展

slice := []int{0, 1, 2, 3, 4}

//只有2个元素0、1
slice1 := slice[0:2]

//使用slice时只能使用slice本身的索引,不能越过slice的边界使用底层数组的索引,否则会报越界
//slice1[2] = 10

//slice不能向前扩展,但可以向后扩展。append()会将元素添加到slice尾部,即底层数组中slice的后面
//返回的是新分片,原分片不变
slice1 = append(slice1, 2, 3, 4)
fmt.Println(slice1)
//切片属于引用类型,已声明、未赋值时默认为nil,可以把nil理解为创建了容器对象,但这个容器中没有元素、是空的
var slice []int

//true
fmt.Print(slice == nil)

for i := 0; i < 100; i++ {
	fmt.Println(len(slice), cap(slice), slice)
	slice = append(slice, i)
}

 

slice的常见操作

//slice可以从数组、其它slice创建,也可以使用make()直接创建,会初始化各元素为该类型的默认值
//第一个参数指定slice的len,第二个参数指定底层数组的cap,第二个参数可选
slice1 := make([]int, 10)
slice2 := make([]int, 10, 20)

//修改元素
slice1[0] = 1

//在末尾添加元素
slice1 = append(slice1, 1)

//删除头部几个元素
slice1 = slice1[2:]
//删除尾部几个元素
slice1 = slice1[:len(slice1)-2]
source := []int{1, 3, 5, 7, 9}
target := []int{0, 2, 4, 6, 8, 10}

//复制分片元素,第一个参数指定目标slice,第二个参数指定源slice
//会将source中元素都复制到target中,是覆盖并非添加,如果target中放不下,不会自动扩容,放不下的部分就不要了
copy(target, source)

 

集合 Map
// map[key类型]value类型。支持复合map,value也可以是一个map
m1 := map[string]string{
	"name": "chy",
	//最后一个元素末尾需要加逗号
	"age": "20",
}

//通过make创建时,创建的是空map,!=nil
m2 := make(map[string]string)

//已声明、未赋值的的map,==nil
var m3 map[string]string


//获取元素个数
count := len(m1)


//获取key对应的value。如果key不存在,会返回对应类型的默认初始值作为value
//name := m1["name"]

//所以一般用2个变量接收返回值,第一个是value,第二个是bool型 标识key是否存在
name, isExist := m1["name"]
fmt.Println(isExist)
if isExist {
	//key存在
	fmt.Println(name)
} else {
	//key不存在
}


//删除指定的键值对
delete(m1, "name")
  • map是无序的
  • map使用哈希表存储元素,但不需要手动实现hashCode()、equals()方法
  • 除slice、map、function以外的内建类型,以及成员字段均不是这三种类型的struct,都可以作为key

 

类型转换

go不支持类型的隐式转换,eg. 不会自动将0、1、空串等转换为bool型,需要手动转换。

var a float32=1.5

//目标类型(变量名),不能直接 目标类型(值)
var b int=int(a)

fmt.Println(a,b)

 

类型扩展

golang没有继承,可以通过组合、定义别名来扩展现有类型。
 
组合

type Node struct {
	Value     int
	Pre, Next *node
}

type NodeExt struct {
	Node  *node
	Score float32
}

func main() {
	node := Node{10, nil, nil}
	//如果定义中没有使用指针(加*),此处就不用&取地址,直接传入即可,但这种对内部字段的修改不会影响|同步到实参
	nodeExt := NodeExt{&node, 10.0}
	fmt.Println(nodeExt)
}

 
定义别名

//定义别名,Queue是[]int的别名
type Queue []int

//结构体的字段如果要暴露给其它包访问,需要将字段名首字母大写;如果要把方法暴露给其他包使用,只需将方法名首字母大写,不需要将形参名首字母大写
func (q *Queue) Push(value int) {
	*q = append(*q, value)
}

func (q *Queue) Pop() int {
	head := (*q)[0]
	*q = (*q)[1:]
	return head
}

func (q *Queue) IsEmpty() bool {
	return len(*q) == 0
}

func main() {
	queue := Queue{}
	queue.Push(10)
	fmt.Println(queue.Pop())
	fmt.Println(queue.IsEmpty())
}