Go八股(Ⅳ)***slice,string,defer***

***slice,string,defer***

1.slice和arry的区别

arry:

Go语言中arry即为数据的一种集合,需要在声明时指定容量和初值,且一旦声明就长度固定,访问时按照索引访问。通过内置函数len可以获取数组中的元素个数。

初始化

数组在初始化时必须指定大小和初值,不过Go语言为我们提供了其他灵活的方式。

例如:

func main() {var arr [5]int //声明了一个大小为5的数组,初始化值为{0,0,0,0,0}arr := [5]int{1}//声明并初始化一个大小为5的数组,初始值为{1,0,0,0,0}arr := [...]int{1, 2, 3}//通过“...”自动获取数组长度,初始化后值为{1,2,3}arr := [...]int{4:1}//指定序列号为4的元素数值为1,初始值为{0,0,0,0.1}
}

数组作参数传入

Go语言数组作为参数传入时,必须指定参数数组的大小,且传入的大小必须与指定的大小一致,数组为按值传递的,函数内对数组的值的改变不影响初始数组

例如:

package mainimport "fmt"func PrintArry(arr [5]int) {arr[0] = 5fmt.Println(arr)
}func main() {var arr [5]int = [5]int{1, 2, 3, 4, 5}PrintArry(arr)fmt.Println(arr)
}

运行结果确实这样的:

Slice

切片是Go语言中极为重要的一种数据类型,可以理解为动态长度的数组(虽然实际上Slice结构内包含了一个数组),访问时可以按照数组的方式访问,也可以通过切片操作访问。Slice有三个属性:指针、长度和容量。指针即Slice名,指向的为数组中第一个可以由Slice访问的元素;长度指当前slice中的元素个数,不能超过slice的容量;容量为slice能包含的最大元素数量,但实际上当容量不足时,会自动扩充为原来的两倍。通过内置函数lencap可以获取slice的长度和容量

初始化

Slice在初始化时需要初始化指针,长度和容量,容量未指定时将自动初始化为长度的大小。可以通过获取数组的引用,获取数组/Slice的切片构建或是make函数初始化数组。

例如

s:=[]int{1,2,3}//通过数组的引用初始化,值为{1,2,3},长度和容量为3arr:=[5]int{1,2,3,4,5}
s:=arr[0:3] //通过数组的切片初始化,值为{1,2,3},长度和容量为5s:=make([]int,4)//通过make初始化,值为{0,0,0,0},长度和容量wei4s:=make([]int,3,5)//通过make初始化值为{0,0,0},长度为3,容量为5

气质特别要注意的时通过切片方式初始化。若时通过对Slice的切片进行初始化,实际上初始化之后的结构如图所示:

此时x的值为[2,3,5,7,11],y的值为[3,5,7],且两个slice的指针指向的是同一个数组,也即x中的元素的值的改变将会导致y中的值也一起改变

这样的初始化方式可能会导致内存被过度占用,如只需要使用一个极大的数组中的几个元素,但是由于需要指向整个数组,所以整个数组在GC时都无法被释放,一直占用内存空间。故使用切片操作进行初始化时,最好使用append函数将切片出来的数据复制到一个新的slice中,从而避免内存占用陷阱。

Slice作为函数参数

Go语言中Slice作为函数参数传递时为按引用传递的,函数内对Slice内元素的修改将导致函数外的值也发生改变,不过由于传入函数的时一个指针的副本,所以对该指针的修改不会导致原来的指针的变化(例如append不会改变原来slice的值)。

例如

func PrintSlice(s []int) {s = append(s, 4)s[0] = -1fmt.Println(s)
}func main() {s := []int{1, 2, 3, 4, 5}s1 := s[0:3]fmt.Println("s:", s)fmt.Println("s1:", s1)PrintSlice(s1)fmt.Println("s:", s)fmt.Println("s1:", s1)
}

总的来说

  • 数组长度不能改变,初始化后长度就是固定的;切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
  • 结构不同,数组是一串固定数据,切片描述的是截取数组的一部分数据,从概念上说是一个结构体。
  • 初始化方式不同,如上。另外在声明时的时候:声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符。
  • unsafe.sizeof的取值不同,unsafe.sizeof(slice)返回的大小是切片的描述符,不管slice里的元素有多少,返回的数据都是24字节。unsafe.sizeof(arr)的值是在随着arr的元素的个数的增加而增加,是数组所存储的数据内存的大小。
  • 函数调用时的传递方式不同,数组按值传递,slice按引用传递。

Slice的扩容机制

1.18版本之前

Go1.18之前切片的扩容是以容量1024为临界点,当旧容量 < 1024个元素,扩容变成2倍;当旧容量 > 1024个元素,那么会进入一个循环,每次增加25%直到大于期望容量。

1.18版本之后

当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。

有什么好处呢,首先是双倍容量扩容的最大阈值从1024降为了256,只要超过了256,就开始进行缓慢的增长。其次是增长比例的调整,之前超过了阈值之后,基本为恒定的1.25倍增长,而现在超过了阈值之后,增长比例是会动态调整的,随着切片容量的变大,增长比例逐渐向着1.25进行靠近

内存对齐

以下是内存对齐得源码;

switch {// 当数组元素的类型大小为1时,不需要乘除计算就能够得到所需要的值  case et.size == 1:lenmem = uintptr(old.len)newlenmem = uintptr(cap)//前面两个语句只是对老长度和预期cap的类型转换,关键是下一个语句决定了newcap的长度// 内存对齐capmem = roundupsize(uintptr(newcap))// 判断是否溢出overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)// 当类型大小是8个字节时  case et.size == sys.PtrSize:lenmem = uintptr(old.len) * sys.PtrSizenewlenmem = uintptr(cap) * sys.PtrSizecapmem = roundupsize(uintptr(newcap) * sys.PtrSize)overflow = uintptr(newcap) > maxAlloc/sys.PtrSizenewcap = int(capmem / sys.PtrSize)// 当类型大小是2的幂次方时  case isPowerOfTwo(et.size):var shift uintptrif sys.PtrSize == 8 {// Mask shift for better code generation.shift = uintptr(sys.Ctz64(uint64(et.size))) & 63} else {shift = uintptr(sys.Ctz32(uint32(et.size))) & 31}lenmem = uintptr(old.len) << shiftnewlenmem = uintptr(cap) << shiftcapmem = roundupsize(uintptr(newcap) << shift)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)// 当大小不是上面任何一种时  default:lenmem = uintptr(old.len) * et.sizenewlenmem = uintptr(cap) * et.sizecapmem, overflow = math.MulUintptr(et.size, uintptr(newcap))capmem = roundupsize(capmem)newcap = int(capmem / et.size)}

之所以进行内存对齐,是因为更加合理得分配内存,如果分配得太多就会出现内存得浪费,如果分配得太少就会出现性能过低情况。

_MaxSmallSize: 其值为32768,即32kb大小。在Go中,当对象大小超过32kb时,内存分配策略和小于等于32kB时是有区别的。(对于内存大于32KB的称为大对象,会单独处理,对于内存小于等于32KB的对象,会在跨度类数组中找到合适的数组大小,其实这一步也就进行了内存对齐操作,找到了最小的对齐内存,所以往往newcap大小会比之前的稍有不同,一般都是向上取了一些值)
smallSizeMax: 其值为1024字节。
smallSizeDiv: 其值为8字节。
largeSizeDiv: 其值为128字节。
_PageSize: 8192字节,即8kb大小。Go按页来管理内存,而每一页的大小就为8kb。
class_to_size:Go中的内存分配会按照不同跨度(也可理解为内存大小,有点类似于段),其中跨度是指,go每一页的大小是8kb,对datablock划分成不同大小的内存块,
除了最小的8b,其余的大小都是8*2n,即8,16,32,48,…32768,具体规则间隔为8,16,32,64,128…,对应class_to_size的数组(1.18之后好像多了一个24元素)
将内存分割成不同内存块链表。当需要分配内存时,按照对象大小去匹配最合适的跨度找到空闲的内存块儿。Go中总共分为67个跨度,class_to_size是一个长度为68的数组,分别记录0和这67个跨度的值。
size_to_class8: 这是一个长度为129的数组,代表的内存大小区间为0~1024字节。以索引i为例,此位置的对象大小m为i
smallSizeDiv,size_to_class8[i]的值为class_to_size数组中跨度最接近m的下标。
size_to_class128:这是一个长度为249的数组,代表的内存大小区间为1024~32768字节。以索引i为例,此位置的对象大小m为smallSizeMax
i*largeSizeDiv, size_to_class128[i]的值为class_to_size数组中跨度最接近m的下标。
divRoundUp: 此函数返回a/b向上舍入最接近的整数。
alignUp: alignUp(size, _PageSize) = _PageSize * divRoundUp(size,
_PageSize)。

 上面得一大块内容,简而言之就是Go语言未来更好得分配内存,将每次扩容得量划分为67个区间

例如:

s3 := []int{1, 2}
s3 = append(s3, 3, 4, 5)
fmt.Println(cap(s3))

根据前文知,所需容量为5,又因所需容量大于2倍当前容量,故新容量也为5。

又因为int类型大小为8(等于64位平台上的指针大小),所以实际需要的内存大小为5 * 8 = 40字节。而67个跨度中最接近40字节的跨度为48字节,所以实际分配的内存容量为48字节。

最终计算真实的容量为48 / 8 = 6,和实际运行输出一致。

零切片,空切片,nil切片的区别

零切片

简单来说就是切片中的值都为0,切片已经分配空间,并且值也不为空

// 创建零切片
slice4 := make([]int,2,5)
fmt.Println(slice4,*(*reflect.SliceHeader)(unsafe.Pointer(&slice4))) // 输出:[0 0] {824634474496 2 5}

空切片

空切片就是已经初始化过空间的切片,但是切片中并没有内容

通常用make或者字面量进行初始化

s1 := []int{} // s1 是一个空切片,通过字面量创建
s2 := make([]int, 0) // s2 也是一个空切片,通过 make 创建

nil切片

通常使用var 来定义,既没有分配空间,更不用说切片的长度

var slice []int
fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 输出:[] {0 0 0}

 string类型

string标准概念

在go的标准包中定义如下:

// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
  • string是8bit字节的集合,通常是但并不一定非得是UTF-8编码的文本。
  • string可以为空(长度为0),但不会是nil。
  • string对象不可以修改。
type stringStruct struct {str unsafe.Pointer		//字符串首地址,指向底层字节数组的指针len int					//字符串长度
}

对于字符串Hello,实际底层结构如下:

3.string类型的操作

3.1  声明

var str string
str = "Hello"

具体的字符串构建过程,是先根据字符串构建stringStruct,再转换成string:

func gostringnocopy(str *byte) string {//根据字符串地址构建stringss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)} //先构造stringStructs := *(*string)(unsafe.Pointer(&ss))//再将stringStruct转换成stringreturn s
}

3.2.1  []byte转string

[]byte切片转换成string很简单(语法上):

func GetStringBySlice(s []byte) string {return string(s)
}

下面是转化时的内存图:

转换过程如下几步:

  1. 根据切片的长度申请内存空间,假设内存地址为p,切片长度为len(s)
  2. 构建string(sting.str =p; string.len=len)
  3. 拷贝数据(切片中数据拷贝到新申请的内存空间)

 3.2.2 string类型转[]byte

 下面是转化的代码,语法上很简单

func GetSliceByString(str string) []byte {return []byte(str)
}

同样string类型转化成[]byte类型也需要一次内存的拷贝。

1.申请切片内存空间

2.将string拷贝到切片

3.3 字符串的拼接

在Go语言中,字符串是不可变得,拼接字符串事实上是创建了一个新的字符串,如果代码中存在大量的字符串拼接,对性能会产生影响。

下面是go语言中关于拼接字符串的源码:

func concatstrings(buf *tmpBuf, a []string) string {idx := 0l := 0 //拼接后的字符串总长度count := 0for i, x := range a {n := len(x)if n == 0 {continue}if l+n < l {throw("string concatenation too long")}l += ncount++idx = i}if count == 0 {return ""}// If there is just one string and either it is not on the stack// or our result does not escape the calling frame (buf != nil),// then we can return that string directly.if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {return a[idx]}s, b := rawstringtmp(buf, l)//生成指定大小的字符串,返回一个string和切片,二者共享内存for _, x := range a {copy(b, x)b = b[len(x):]//string无法修改,只能通过切片修改}return s
}// 生成一个新的string,返回的string和切片共享相同的空间
func rawstring(size int) (s string, b []byte) {p := mallocgc(uintptr(size), nil, false)stringStructOf(&s).str = pstringStructOf(&s).len = size*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}return
}

3.3.1常见的拼接方式

使用“+”

s1+s2+s3

使用fmt.Sprintf

fmt.Sprintf("%s%s",s1,s2)

使用strings.Builder

func BuilderConcat(n int, str string) string {var builder strings.Builderfor i := 0; i < n; i++ {builder.WriteString(str)}return builder.String()
}

使用bytes.Buffer

func bufferConcat(n int, s string) string {buf := new(bytes.Buffer)for i := 0; i < n; i++ {buf.WriteString(s)}return buf.String()
}

 使用[]byte

func byteConcat(n int, str string) string {buf := make([]byte, 0)for i := 0; i < n; i++ {buf = append(buf, str...)}return string(buf)
}

3.4 字符串的截取

1.截取普通英语字符串

str := "HelloWorld"
content := str[1 : len(str)-1] 

2.截取带中文的字符串

一个中文字符确定不止一个字节,需要先将其转为[]rune,再截取后,再转为string

strRune := []rune(str)
fmt.Println("string(strRune[:4]) = ",string(strRune[:4]))

4.为什么字符串不允许修改(只读属性)

在go实现中,string不包含内存空间,只有一个内存的地址,这样做的好处是string变得非常轻量,可以很方便的进行传递而不用担心内存拷贝。

string通常指向字符串字面量,而字符串字面量存储存储位置是只读段,而不是堆或栈上,所以string不可修改。

修改字符串时,可以将字符串转换为 []byte 进行修改。

var str string = "hello"
strBytes := []byte(str)
strBytes[0] = 'H'
str = string(strBytes)
fmt.Println(str)

defer

defer

一个函数中多个defer的执行顺序

defer 的作用就是把defer关键字之后的函数执行压入一个栈中延迟执行,多个defer的执行顺序是后进先出LIFO,也就是先执行最后一个defer,最后执行第一个defer

func main() {defer fmt.Println(1)defer fmt.Println(2)defer fmt.Println(3)
}

return返回值的运行机制

1.返回值赋值

2.RET指令

而defer执行在赋值之后,RET之前。

defer,return,返回值三者执行的顺序是:return最先执行,先将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带返回值退出

 不带命名返回值

如果函数的返回值是无名的(不带命名的返回值),则go语言会在执行return的时候执行一个类似创建一个临时变量作为保存return值得动作。

func main() {fmt.Println("return i:", test())
}func test() int {i := 0defer func() {i++fmt.Println("defer1 ---i:", i)}()defer func() {i++fmt.Println("defer2 ---i:", i)}()return i
}

运行结果如下图所示:

 

如图所示,函数执行时先返回值然后再执行defer之后得函数。

上面得例子实际上进行了三步操作:

(1)赋值,因为返回值没有命名,所以return默认指定了一个返回值(假设为s),首先将i赋值为s,i初始值是0,所以s也是0

(2)后续的defer操作因为是针对i进行的,所以不会影响s,此后s不会更新,所以s还是0

(3)返回值,return s,也就是return 0

var i int

s:=i

return s

带命名的返回值

有名返回值的函数,由于返回值在函数定义的时候已经将该变量进行定义,在执行return的时候会先执行返回值保存操作,而后续的defer函数会改变这个给返回值(虽然defer实在return之后执行的),由于使用函数定义的变量,所以执行defer操作后会对该变量的修改会影响的return 的值

func main() {fmt.Println("return i:", test())
}func test() (i int) {defer func() {i++fmt.Println("defer1 ---i:", i)}()defer func() {i++fmt.Println("defer2 ---i:", i)}()return i
}

运行结果如下;

这种情况其实就相当于一直在操作一个内存地址中的数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/466646.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用 Mac 数据恢复从 iPhoto 图库中恢复照片

我们每个人都会遇到这种情况&#xff1a;在意识到我们不想丢失照片之前&#xff0c;我们会永久删除 iPhoto 图库中的一些照片。永久删除这些照片后&#xff0c;是否可以从 iPhoto 图库中恢复照片&#xff1f;本文将指导您使用免费的 Mac 数据恢复软件从 iPhoto 中恢复照片。 i…

Spark 的介绍与搭建:从理论到实践

目录 一、分布式的思想 &#xff08;一&#xff09;存储 &#xff08;二&#xff09;计算 二、Spark 简介 &#xff08;一&#xff09;发展历程 &#xff08;二&#xff09;Spark 能做什么&#xff1f; &#xff08;三&#xff09;spark 的组成部分 &#xff08;四&…

Spring Boot2(Spring Boot 的Web开发 springMVC 请求处理 参数绑定 常用注解 数据传递 文件上传)

SpringBoot的web开发 静态资源映射规则 总结&#xff1a;只要静态资源放在类路径下&#xff1a; called /static (or /public or /resources or //METAINF/resources 一启动服务器就能访问到静态资源文件 springboot只需要将图片放在 static 下 就可以被访问到了 总结&…

Vue2中使用firefox的pdfjs进行文件文件流预览

文章目录 1.使用场景2. 使用方式1. npm 包下载,[点击查看](https://www.npmjs.com/package/pdfjs-dist)2. 官网下载1. 放到public文件夹下面2. 官网下载地址[点我,进入官网](https://github.com/mozilla/pdf.js/tags?afterv3.3.122) 3. 代码演示4. 图片预览5. 如果遇到跨域或者…

2024软件测试面试热点问题

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 大厂面试热点问题 1、测试人员需要何时参加需求分析&#xff1f; 如果条件循序 原则上来说 是越早介入需求分析越好 因为测试人员对需求理解越深刻 对测试工…

C语言 | Leetcode C语言题解之第542题01矩阵

题目&#xff1a; 题解&#xff1a; /*** Return an array of arrays of size *returnSize.* The sizes of the arrays are returned as *returnColumnSizes array.* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().*/ type…

C++总结

目录 一、面向对象的三大特性 二、引用 2.1 概念 2.2特性 三、类与对象 3.1概念 3.2 类的内容 3.3对象的创建 四、构造函数与析构函数 五、封装 六、继承 6.1概念与基础使用 6.2 继承权限 6.2.1 权限修饰符 6.2.2 继承权限 6.3构造函数 6.3.1 派生类与基类的构造函数关系 6.3.2…

2024 CSS保姆级教程二 - BFC详解

前言 - CSS中的文档流 在介绍BFC之前&#xff0c;需要先给大家介绍一下文档流。​ 我们常说的文档流其实分为定位流、浮动流、普通流三种。​ ​ 1. 绝对定位(Absolute positioning)​ 如果元素的属性 position 为 absolute 或 fixed&#xff0c;它就是一个绝对定位元素。​ 在…

在PHP8内,用Jenssegers MongoDB扩展来实现Laravel与MongoDB的集成

在现代 web 开发中&#xff0c;MongoDB 作为一种流行的 NoSQL 数据库&#xff0c;因其灵活的文档结构和高性能而受到许多开发者的青睐。Laravel&#xff0c;作为一个优雅的 PHP Web 框架&#xff0c;提供了丰富的功能和优雅的代码风格。本文将指导你如何在 Laravel 项目中集成 …

GPU 环境搭建指南:如何在裸机、Docker、K8s 等环境中使用 GPU

本文主要分享在不同环境&#xff0c;例如裸机、Docker 和 Kubernetes 等环境中如何使用 GPU。 跳转阅读原文&#xff1a;GPU 环境搭建指南&#xff1a;如何在裸机、Docker、K8s 等环境中使用 GPU 1. 概述 仅以比较常见的 NVIDIA GPU 举例&#xff0c;系统为 Linux&#xff0c;…

Axure设计之左右滚动组件教程(动态面板)

很多项目产品设计经常会遇到左右滚动的导航、图片展示、内容区域等&#xff0c;接下来我们用Axure来实现一下左右滚动的菜单导航。通过案例我们可以举一反三进行其他方式的滚动组件设计&#xff0c;如常见的上下滚动、翻页滚动等等。 一、效果展示&#xff1a; 1、点击“向左箭…

每天五分钟深度学习框架pytorch:如何加载手写字体数据集mnist?

本文重点 那个这节课程之后,我们就将通过代码的方式来搭建CNN和RNN模型,然后训练,我们使用的数据集为pytorch中已经封装好的数据集,比如mnist,cafir10,本文我们学习一下如何在pytorch中使用它们,然后为之后的章节做准备,现在我们拿mnist来举例。 mnist和cafir10 MIN…

itextpdf打印A5的问题

使用A5打印的时候&#xff0c;再生成pdf是没有问题的。下面做了一个测试&#xff0c;在打印机中&#xff0c;使用A5的纸张横向放入&#xff0c;因为是家用打印机&#xff0c;A5与A4是同一个口&#xff0c;因此只能这么放。 使用itextpdf生成pdf&#xff0c;在浏览器中预览pdf是…

AJAX 全面教程:从基础到高级

AJAX 全面教程&#xff1a;从基础到高级 目录 什么是 AJAXAJAX 的工作原理AJAX 的主要对象AJAX 的基本用法AJAX 与 JSONAJAX 的高级用法AJAX 的错误处理AJAX 的性能优化AJAX 的安全性AJAX 的应用场景总结与展望 什么是 AJAX AJAX&#xff08;Asynchronous JavaScript and XML…

CKA认证 | Day1 k8s核心概念与集群搭建

第一章 Kubernetes 核心概念 1、主流的容器集群管理系统 容器编排系统&#xff1a; KubernetesSwarmMesos Marathon 2、Kubernetes介绍 Kubernetes是Google在2014年开源的一个容器集群管理系统&#xff0c;Kubernetes简称K8s。 Kubernetes用于容器化应用程序的部署&#x…

web实操1——只使用tomcat发布网站

安装tomcat 下载 肯定是去官网&#xff1a; http://tomcat.apache.org/ 下载之后&#xff0c;解压&#xff1a; &#xff01;&#xff01;解压后&#xff1a; logs日志&#xff1a;就是一些输出&#xff0c;输到文本里。 temp:一些临时文件(不用管) webapps:放网站的 work&…

数据结构:七种排序及总结

文章目录 排序一插入排序1直接插入排序2希尔排序二选择排序3直接选择排序4堆排序三 交换排序5冒泡排序6快速排序四 归并排序7归并排序源码 排序 我们数据结构常见的排序有四大种&#xff0c;四大种又分为七小种&#xff0c;如图所示 排序&#xff1a;所谓排序&#xff0c;就是…

A day a tweet(sixteen)——The better way of search of ChatGPT

Introducing ChatGPT search a/ad.及时的/及时地 ChatGPT can now search the web in a much better way than before so you get fast, timely a.有关的(relative n.亲戚,亲属;同类事物 a.比较的&#xff1b;相对的) answers with link…

HTMLCSS:呈现的3D树之美

效果演示 这段代码通过HTML和CSS创建了一个具有3D效果的树的图形&#xff0c;包括分支、树干和阴影&#xff0c;通过自定义属性和复杂的变换实现了较为逼真的立体效果。 HTML <div class"container"><div class"tree"><div class"…

系统规划与管理师——第十二章 职业素养与法侓法规

目录 职业素养 职业道德 行为规范 职业责任 对客户和公众的责任 法律法规 法律概念 法律体系 诉讼时效 民事诉讼时效 刑事追诉时效 常用的法律法规 合同法 招投标法 著作权法 政府采购法 劳动法 知识产权法 刑法修正案&#xff08;七) IT服务的广泛应用不仅…