Go 是一门简单有趣的编程语言,与其他语言一样,在使用时不免会遇到很多坑,不过它们大多不是 Go 本身的设计缺陷。如果你刚从其他语言转到 Go,那这篇文章里的坑多半会踩到。
如果花时间学习官方 doc、wiki、讨论邮件列表、 Rob Pike 的大量文章以及 Go 的源码,会发现这篇文章中的坑是很常见的,跳过这些坑,能减少大量调试代码的时间。
如果在函数体代码中有未使用的变量,则无法通过编译,不过全局变量声明但不使用是可以的。即使变量声明后为变量赋值,依旧无法通过编译,需在某处使用它:
// 可以直接注释或移除未使用的变量如果你 import一个包,但包中的变量、函数、接口和结构体一个都没有用到的话,将编译失败。可以使用 _下划线符号作为别名来忽略导入的包,从而避免编译错误,这只会执行 package 的 init()
// 可以使用 goimports 工具来注释或移除未使用到的包不能用简短声明方式来单独为一个变量重复声明,:=左侧至少有一个新变量,才允许多变量的重复声明:
struct 的变量字段不能使用 := 来赋值以使用预定义的变量来避免解决:
对从动态语言转过来的开发者来说,简短声明很好用,这可能会让人误会 := 是一个赋值操作符。如果你在新的代码块中像下边这样误用了 :=,编译不会报错,但是变量不会按你的预期工作:
这是 Go 开发者常犯的错,而且不易被发现。可使用 vet工具来诊断这种变量覆盖,Go 默认不做覆盖检查,添加 -shadow 选项来启用:
注意 vet 不会报告全部被覆盖的变量,可以使用 go-nyet 来做进一步的检测:
在创建 map 类型的变量时可以指定容量,但不能像 slice 一样使用 cap() 来检测分配空间的大小:
对那些喜欢用 nil 初始化字符串的人来说,这就是坑:
在 C/C++ 中,数组(名)是指针。将数组作为参数传进函数时,相当于传递了数组内存地址的引用,在函数内部会改变该数组的值。
在 Go 中,数组是值。作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的:
// 数组使用值拷贝传参
// 传址会修改原数据
与其他编程语言中的 for-in 、foreach 遍历语句不同,Go 中的 range 在遍历时会生成 2 个值,第一个是元素索引,第二个是元素的值:
看起来 Go 支持多维的 array 和 slice,可以创建数组的数组、切片的切片,但其实并不是。
对依赖动态计算多维数组值的应用来说,就性能和复杂度而言,用 Go 实现的效果并不理想。
可以使用原始的一维数组、“独立“ 的切片、“共享底层数组”的切片来创建动态的多维数组。
1.使用原始的一维数组:要做好索引检查、溢出检测、以及当数组满时再添加值时要重新做内存分配。
2.使用“独立”的切片分两步:
对每个内部 slice 进行内存分配
注意内部的 slice 相互独立,使得任一内部 slice 增缩都不会影响到其他的 slice
1.使用“共享底层数组”的切片
和其他编程语言类似,如果访问了 map 中不存在的 key 则希望能返回 nil,比如在 PHP 中:
Go 则会返回元素对应数据类型的零值,比如 nil、'' 、false 和 0,取值操作总有值返回,故不能通过取出来的值来判断 key 是不是在 map 中。
检查 key 是否存在可以用 map 直接访问,检查返回的第二个参数即可:
尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许的。
// 修改字符串的错误示例
注意: 上边的示例并不是更新字符串的正确姿势,因为一个 UTF8 编码的字符可能会占多个字节,比如汉字就需要 3~4个字节来存储,此时更新其中的一个字节是错误的。
当进行 string 和 byte slice 相互转换时,参与转换的是拷贝的原始值。这种转换的过程,与其他编程语的强制类型转换操作不同,也和新 slice 与旧 slice 共享底层数组不同。
Go 在 string 与 byte slice 相互转换上优化了两点,避免了额外的内存分配:
对字符串用索引访问返回的不是字符,而是一个 byte 值。
string 的值不必是 UTF8 文本,可以包含任意的值。只有字符串是文字字面值时才是 UTF8 文本,字串可以通过转义来包含其他数据。
注意: RuneCountInString 并不总是返回我们看到的字符数,因为有的字符会占用 2 个 rune:
声明语句中 } 折叠到单行后,尾部的 , 不是必需的。
log 标准库提供了不同的日志记录等级,与其他语言的日志库不同,Go 的 log 包在调用 Fatal()、Panic() 时能做更多日志外的事,如中断程序的执行等:
尽管 Go 本身有大量的特性来支持并发,但并不保证并发的数据安全,用户需自己保证变量等数据以原子操作更新。
range 得到的索引是字符值(Unicode point / rune)第一个字节的位置,与其他编程语言不同,这个索引并不直接是字符在字符串中的位置。
注意一个字符可能占多个 rune,比如法文单词 café 中的 é。操作特殊字符可使用norm 包。
如果你希望以特定的顺序(如按 key 排序)来迭代 map,要注意每次迭代都可能产生不一样的结果。
Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。但也并不总会打乱,得到连续相同的 5 个迭代结果也是可能的,如:
如果你去 Go Playground 重复运行上边的代码,输出是不会变的,只有你更新代码它才会重新编译。重新编译后迭代顺序是被打乱的:
不过你可以在 case 代码块末尾使用 fallthrough,强制执行下一个 case 代码块。
也可以改写 case 为多条件判断:
很多编程语言都自带前置后置的 ++、-- 运算。但 Go 特立独行,去掉了前置操作,同时 ++、— 只作为运算符而非表达式。
很多编程语言使用 ~ 作为一元按位取反(NOT)操作符,Go 重用 ^ XOR 操作符来按位取反:
同时 ^ 也是按位异或(XOR)操作符。
除了位清除(bit clear)操作符,Go 也有很多和其他语言一样的位操作符,但优先级另当别论。
以小写字母开头的字段成员是无法被外部直接访问的,所以 struct 在进行 json、xml、gob 等格式的 encode 操作时,这些私有字段会被忽略,导出时得到零值:
程序默认不等所有 goroutine 都执行完才退出,这点需要特别注意:
// 主程序会直接退出
如下,main() 主程序不等两个 goroutine 执行完就直接退出了:
常用解决办法:使用 "WaitGroup" 变量,它会让主程序等待所有 goroutine 执行完毕再退出。
如果你的 goroutine 要做消息的循环处理等耗时操作,可以向它们发送一条 kill 消息来关闭它们。或直接关闭一个它们都等待接收数据的 channel:
看起来好像 goroutine 都执行完了,然而报错:
为什么会发生死锁?goroutine 在退出前调用了 wg.Done() ,程序应该正常退出的。
只有在数据被 receiver 处理时,sender 才会阻塞。因运行环境而异,在 sender 发送完数据后,receiver 的 goroutine 可能没有足够的时间处理下一个数据。如:
从已关闭的 channel 接收数据是安全的:
接收状态值 ok 是 false 时表明 channel 中已没有数据可以接收了。类似的,从有缓冲的 channel 中接收数据,缓存的数据获取完再没有数据可取时,状态值也是 false
在一个值为 nil 的 channel 上发送和接收数据将永久阻塞:
利用这个死锁的特性,可以用在 select 中动态的打开和关闭 case 语句块:
方法 receiver 的参数与一般函数的参数类似:如果声明为值,那方法体得到的是一份参数的值拷贝,此时对参数的任何修改都不会对原有值产生影响。
除非 receiver 参数是 map 或 slice 类型的变量,并且是以指针方式更新 map 中的字段、slice 中的元素的,才会更新原有值:
公司:程序咖(北京)科技有限公司
/ruby0015/冒泡排序、选择排序、顺序查找和二分法查找)
1.1 为什么使用数组
如果说程序中,需要存储大量的相同类型的一组数据,如果直接使用变量来进行存储,每个变量只能存储一个值,就需要大量的变量。
就是一组相同数据类型的数据。内存上是开辟的连续的内存空间。
1、定长。容量固定。数组一旦创建后,那么长度不能更改。(容量,长度,都是指存储的数量)
2、存储的数据类型必须都一致。
3、在内存中空间连续挨个。
4、数组是引用类型的数据,存在栈和堆的地址引用关系。
Java中:两大数据类型
引用:数组,对象,集合。。。。
step1:先创建数组
step2:使用数组:存储数据,访问数据。
方式一:数据类型[] 数组名
方式二:数据类型 数组名[]
推荐使用方式一,c#等越来越多的语言已经不支持方式二定义数组
创建好数组后,里面存储了默认的数值。到底存储哪个数值,要看创建的数组是何种类型。
数组的默认值,就是数组创建后,里面存储的默认的数据。
数组的引用存在栈内存中,数组本身存在堆内存中。
数组创建完,就有默认的数据了。
Java中的数组必须先初始化,然后才可以使用,所谓初始化,就是为数组中的数组元素分配内存空间,并为每个数组元素赋初始值。否则数组中存储的就是默认数值。
初始化时由程序员指定每个数组元素的初始值,由系统计算数组长度
语法:数组元素类型[] 数组名 = new 数组元素类型[]{元素0,元素1,....};
可简写为:数组元素类型[] 数组名 = {元素0,元素1,....};
说明:任何一个变量都得有自己的数据类型,这里的arr表示数组变量名称,int表示数组中元素的类型,int[]才是数组类型
初始化时程序员只指定数组长度,由系统为数组元素分配初始值
语法:元素类型[] 数组名 = new 元素类型[元素个数或者数组长度];
系统对初始值分配规则如下:a.整数型为0
c.字符型为‘\u0000’(不同的系统平台显示结果不同)
动态初始化:初始化时由程序员指定数组的长度,系统负责分配元素的初始值
4为常量,存储在常量池中 arr也是在栈空间中开辟空间的 通过new关键字创建出来的数组存储在堆空间中 arr其实就是一个引用,指向了一个真正的数组
a.在初始化数组时,不要静态初始化和动态初始化同时使用,也就是说,不要在进行数组初始化时,既指定数组的长度,也为每个数组元素分配初始值
b.既然数组也是一种数据类型,则在初始化的时候也可以先声明,再初始化
通过下标访问指定元素,数组中的每个下标,都有编号,顺序,也叫索引,index。从0开始,到个数减1。
比如说一个数组中元素的个数是3个。
我们就可以通过数组的名字,结合下标来进行访问数组中的元素:
//2.修改数组元素的值
//格式:数组名称[下标] = 被修改之后的值
//注意:不管是静态初始化还是动态初始化,都可以采用这种方式修改元素的值
//3.如果下标超出数组的长度,会下标越界异常
数据类型[] 数组名 = new 数据类型[长度、容量、个数]; 数据类型 数组名[] = new 数据类型[长度];//也可以 数组名[index],操作数组
index:因为一个数组存储了多个数据(也叫元素),每个元素都有一个下标,也叫索引,index。理解起来就是给每个数据排个编号,固定从0开始,0,1,2,3,4.。。。到长度减1。
获取数组元素的个数:之前我们知道在访问数组的时候,可以通过下标进行访问,但是如果下标越界就会产生异常。在Java中,所有数组都提供了一个length属性,通过这个属性可以访问到数组的长度或者数组中元素的个数。
//4.获取数组中的元素个数或者数组的长度
既然可以通过数组的下标进行访问数组,而下标又是从0开始,逐渐递增1的有规律的一组数值。那么我们是否可以通过循环来进行访问数组?
依次访问数组中的每一个元素,获取每个下标对应的元素值
方式一:简单for循环
方式二:增强for循环
1、遍历:依次访问数组中每个元素。可以赋值,可以取值。
2、因为操作数组,就是数组名字配合下标,而下标固定都是从0开始,到长度减1。
增强for循环:JDK1.5的版本出现的。 特定的用法:专门用于获取数组中的每一个元素的值。
依次获取数组的元素,赋值给e A:for-each只能获取数组的数据,但是不能给数组进行赋值
B:for-each,在代码这个层面,不能操作下标。
数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存空间,只有当这个引用指向有效的空间时,才可以通过引用去操作真正数组中的元素
结论:数组的引用变量存储在栈空间中,而真正的数组存储在堆空间在中。
扩展:基本数据类型和引用数据类型在内存中的区别
a.寄存器:最快的存储区域,由编译器根据需求进行自动的分配,我们在程序中无法控制
b.栈:存放的是基本数据类型的变量以及引用数据类型变量的引用
特点:被执行之后,该函数或者变量所占用的空间会被销毁【方法压栈】
c.堆:存放所有使用new关键字创建出来的实体
特点:执行完不会立即被释放,当使用完成之后,会被标记上垃圾的标识,等待系统的垃圾回收机制来回收它
常量池:存放基本数据类型的常量和字符串常量
静态域:static,静态全局变量
Java中的数据分为两大类:
基本类型进行赋值:数值
引用类型进行赋值:地址
数组是引用类型:参数传递的时候,传递的是数组的地址。就是参数也会指向这块内存。当方法结束的时候,参数就销毁了。
一个数组可以作为参数,也可以作为返回值。那么返回的实际上是数组的内存地址。
概念:一个方法可以接收的参数的数量不定(0-多个),但是类型固定。
语法:数据类型 ... 参数名,可变参数在方法中当数组使用。
1、如果参数列表中,除了可变参数还有其他的参数,可变参数要写在整个参数列表的最后。
2、一个方法最多只能有一个可变参数。
//演示不定长参数的使用
//2.对于一个方法的参数是不定长参数时,实参可以直接传一个数组
//需求:计算不确定个整数的和
//1.不定长的参数在进行使用的时候被当做数组来进行处理
//num其实就相当于一个数组的引用变量
//3.不定长参数在使用的时候,必须出现在参数列表的最后一个
//4.在同一个参数列表中,不定长参数只能出现一次
a.不定长的参数在进行使用的时候被当做数组来进行处理
b.不定长参数在使用的时候,必须出现在参数列表的最后一个
c.对于一个方法的参数是不定长参数时,实参可以直接传一个数组
排序:数组是存储一组数据,而且这些数据是有顺序的。但是数值本身可能是无序的。通过算法来实现给数组进行排序,升序(数值从小到大),降序(数值从大到小)。
冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
交换过程图示(第一次):
那么我们需要进行n-1次冒泡过程。
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
那么我们需要进行n-1次冒泡过程。
数组的查找,就是指在给定的一个数组中,查找指定的数值,返回该数值在数组中的下标位置。但如果数组中包含重复的元素,不保证找到的是哪一个,如果数组中没有指定的元素,一般返回-1。
查找思路:遍历这个数组,依次把每一位元素和要查找的数据进行比较
二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查数组为有序的数组,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列数组。首先,假设数组中元素是按升序排列,将数组中间位置记录的元素与要查找的数值比较,如果两者相等,则查找成功;否则利用中间位置记录将数组分成前、后两个部分,如果中间位置记录的元素值大于查找的数值,则进一步查找前一部分数组,否则进一步查找后一八月份数组。重复以上过程,直到找到满足条件的记录,使查找成功,或直到部分数组不存在为止,此时查找不成功。
优点:通过折半来缩小查找范围,提高查找效率
缺点:要求数组是有序的。
Arrays类是jdk提供的操作数组的一个工具类,位于java.util包下。
作用:主要用于对数组进行排序,查找,填充,比较等的操作
注意1:如果在同一个Java文件中同时使用Scanner和Arrays,则可以向如下方式导包:
注意2:但凡是工具类,类中的方法全部是静态的,方便调用
调用语法:类名.方法名(实参列表)
D:Arrays,提供数组的常规操作的
数组的拷贝:就是将一个数组的数据,复制到另一个数值中。
方法一:通过循环,依次复制,将原数组的数据,一个一个,复制到目标数组中
方法二:Arrays类里方法:copyOf(原数组,新数组的长度)-->返回值是新数组
方法三:System类里的方法:arraycopy(原数组,原数组位置,新数组,新数组位置,拷贝的个数)
Java并没有真正的多维数组,二维数组可以看成以数组为元素的数组。如:
《JA V A语言程序设计》期末考试试题及答案
1、编译Java Application 源程序文件将产生相应的字节码文件,这些字节码文件的扩展名为( )。
3、不允许作为类及类成员的访问控制符的是( )。
4、为AB类的一个无形式参数无返回值的方法method书写方法头,使得使用类名AB作为前缀就可以调用它,该方法头的形式为( )。
1、开发与运行Java程序需要经过的三个主要步骤为编辑源程序、
编译生成字节码和解释运行字节码。
用2字节内存空间,这样,无论是中文字符还是英文字符,都是占
5、抽象(abstract) 方法是一种仅有方法头,没有具体方法体和操作实现的方法,该方法必须在抽象类之中定义。最终(final)方法是不能被当前类的子类重新定义的方法。
该语句应该放在程序的位置为:应该在程序第一句。