Software Development

How Go lang struct works

This is the 3rd post of my Go lang experiment,. If you want to read the earlier posts then go to:

is-it-worth-learning-golang

what-are-golang-types

Struct are cool types. It allows to create user defined type.

Struct basic

Struct can be declared like this

type person struct {
   firstName string
   lastName string
}

this declares struct with 2 fields.

Struct variables can be declared like this:

var p1 person

var construct will initialized p1 to Zero value, so both the string fields are set to “”.

DOT (.) construct is used to access field.

How to define struct variables.

Couple of ways by which variable can be created.

var p1 person                                      // Zero value
 var p2 = person{}                                  //Zero value
 p3 := person{firstName: "James", lastName: "Bond"} //Proper initialization
 p4 := person{firstName: "James"}                   //Partial initialization

  p5 := new(person) // Using new operator , this returns pointer
 p5.firstName = "James"
 p5.lastName = "Bond"

Struct comparison

Same type of struct can be compared using “==” operator

p1 := person{firstName: "James", lastName: "Bond"}
p2 := person{firstName: "James", lastName: "Bond"}


if p1 == p2 {
  fmt.Println("Same person found!!!!", p1)
 } else {
  fmt.Println("They are different", p1, p2)
 }

this shows the power of pure value, no equals/hashcode type of things are required to compare. The language has first class support to compare by value.

Struct conversion

Go lang does not have casting. It supports conversion and it is applicable to any types not just struct.

Casting keep source object reference and put target object struct/layout on top of it, so in casting any changes done to source object after casting is visible to the target object.

This is good for reducing memory overhead but for safety this can cause big problem because values can change magically from source object.

On other end conversion copies source value, so after conversion both source and target have no link. Changing one does not impact on the other one. This is good for type safety and easy to reason about code.

Lets look into some conversion example of struct.

type person struct {
   firstName string
   lastName string
}

type anotherperson struct {
 firstName string
 lastName  string
}


Both of the above are same in structure but these two can’t be assigned to each other without conversion.

p1 := person{firstName: "James", lastName: "Bond"}
anotherp1 := anotherperson{firstName: "James", lastName: "Bond"}


p1  = anotherp1 //This is compile time error
p1 = person(anotherp1)//This is allowed

The compiler is very smart to figure out that these two types are compatible and conversion is allowed.

Now if go and make change in otherperson struct like drop the field/ new field/change the order then it becomes not compatible and the compiler stops this!

When it does allow conversion then it allocate new memory for target variable and copies the value.

For eg

p1 = person(anotherp1)
anotherp1.lastName = "Lee" // Will have not effect on p1

How struct are allocated

Since it is composite type and understanding memory layout of struct is very useful in knowing what type of overhead it comes up.

Current processor will do some cool things for fast & safe read/write.

Memory allocation will be aligned to word size of underlying platform ( 32 bit or 64 bit) and it will be also aligned based on size of the type for eg 4 byte value will be aligned to 4 byte address.

Alignment is very important for speed and correctness.

Lets take example to understand this, in 64 bit platform word size is 64bit or 8 byte, so it will take 1 instruction to read 1 word.

Go lang struct

Value shown in red is 2 byte and if value shown in red is allocated in 2 words (i.e at the boundary of word) then it is going to take multiple operation to read/write value and for write some kind of synchronization might be required.

Since value is only 2 byte it can easily fit in single word so compiler will try to allocate this in single word:

Go lang struct

Above allocation is optimized for read/write. Struct allocation works on the same principle.

Now lets take an example of a struct and see what will be the memory layout:

type layouttest struct {
 b  byte
 v  int32
 f  float64
 v2 int32
}


Layout of “layoutouttest” will look something like below:

[ 1 X X 1 1 1 1 X ][1 1 1 1 X X X X][1 1 1 1 1 1 1 1][1 1 1 1 X X X X]


X – is for padding.

It took 4 words to place this struct and to get the alignment by data type padding is added.

If we calculate the size of struct ( 1 + 4 + 4 + 8 = 17) then it should fit a value in 3 words ( 8*3 = 24) but it took 4 words( 8 * 4 = 32). It might look like 8 bytes are wasted.

Go gives full control to developer about memory layout. Much more compact struct can be created to get to 3 word allocation.

type compactlyouttest struct {
 f  float64
 v  int32
 v2 int32
 b  byte
}

Above struct has reordered field in descending order by size it takes and this helps in getting to below memory layout

[ 1 1 1 1 1 1 1 1 ][1 1 1 1 1 1 1 1][1 X X X X X X X]

In this arrangement less space is wasted in padding and you might be tempted to use compact representation.

You should should not do this for couple of reason

– This breaks the readability because related fields are moved all over the place.

– Memory might not be issue, so it could be just over optimization.

– Processor are very smart, values are read in cacheline not in word, so CPU will read multiple words and you will never see any slowness in read. You can read about how cache line works in cpu-cache-access-pattern post.

– Over optimization can result in false sharing issue, read concurrent-counter-with-no-false-sharing to see impact of false sharing in multi threaded code.

So profile application before doing any optimization.

Go has built in packages for getting memory alignment details & other static information of types.

Below code gives lot of details about memory layout

var x layouttest
var y compactyouttest



	fmt.Printf("Int alignment %v \n", unsafe.Alignof(10))

	fmt.Printf("Int8 aligment %v \n", unsafe.Alignof(int8(10)))

	fmt.Printf("Int16 aligment %v \n", unsafe.Alignof(int16(10)))

	fmt.Printf("Layoutest aligment %v ans size is %v \n", unsafe.Alignof(x), unsafe.Sizeof(x))

	fmt.Printf("Compactlayouttest aligment %v and size is %v \n", unsafe.Alignof(y), unsafe.Sizeof(y))



	fmt.Printf("Type %v has %v fields and takes %v bytes \n", reflect.TypeOf(x).Name(), reflect.TypeOf(x).NumField(), reflect.TypeOf(x).Size())

Unsafe & reflect package gives a lot of internal details and looks like the idea has come from java.

Code used in this blog is available @ 001-struct github.

Published on Java Code Geeks with permission by Ashkrit Sharma, partner at our JCG program. See the original article here: How Go lang struct works

Opinions expressed by Java Code Geeks contributors are their own.

Ashkrit Sharma

Pragmatic software developer who loves practice that makes software development fun and likes to develop high performance & low latency system.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button