프로그래밍 언어의 특성
Programming languages are fundamentally designed to address the same computing problems. Most of these languages reflect human logical reasoning and the ability to abstract. They often evolve from existing languages. Furthermore, research and standardization efforts in the field of computer science have promoted the development of common paradigms and practices across various languages. Moreover, since most programming languages run on the same computer architecture(Von neumann), they tend to exhibit similar structural and functional characteristics based on these constraints and features.
In this article, we'll delve into the general attributes and functionalities of programming languages.
Variables
A variable is an identifier or name used to store data within computer memory. It provides an abstract representation of the intricate memory address where data is stored, allowing easy access to the data. Variables can hold various forms of data, such as numbers, strings, objects, and functions. The value at the actual memory location pointed to by a variable can change depending on the behavior of the running program. When declaring a variable, you can specify the type of data it will hold (this depends on whether the language uses dynamic or static typing). This type determines the space the variable occupies in memory and the kind of data it holds.
1. Types
A variable's type determines how it's represented in memory and what operations can be applied to it. It's a powerful tool in programming languages, ensuring data is processed as intended by the developer.
Static Typing vs. Dynamic Typing
- Static Typing
At compile-time, a variable's data type is determined, and values outside of this type cannot be assigned to it. This allows many types of errors to be discovered before runtime. C++ and Java are typical examples. Despite being a statically typed language, Go offers type inference capabilities through the := operator, providing flexibility akin to dynamically typed languages. However, Go determines variable types during compilation, allowing for the discovery of errors before runtime. Additionally, its inherent compilation process enables the generation of more efficient machine code.
- Dynamic Typing
The data type of a variable is determined during program execution, and its type can change based on the value assigned to it. This offers flexibility but also increases the risk of runtime errors. Python and JavaScript are examples.
Compiler Languages vs. Interpreter Languages
- Compiled Languages
The source code undergoes a compilation process to be translated into machine code. This process occurs before execution. Most compiled languages use static typing since knowing the type at compile-time allows for the generation of optimized machine code and early detection of type-related errors.
- Interpreted Languages
The source code is interpreted and executed directly at runtime. Many interpreted languages choose dynamic typing.
While statically typed languages typically adopt the compiled approach, and dynamically typed languages lean towards the interpreted method, this isn't an absolute rule. The boundary between these concepts is blurring. For instance, Swift, although statically typed, supports an interpreter mode. On the other hand, JavaScript, a dynamically typed language, has recent engines employing Just-In-Time (JIT) compilation to enhance code performance.
Strong Typing vs. Weak Typing
- Strong Typing
Once determined, a variable's data type doesn't change. Explicit type conversion is required for operations or assignments involving different types. This ensures higher data integrity. Languages like Python, Java, and Go typically fall under this category.
- Weak Typing
Variables of diverse types can be automatically converted, making operations or assignments more flexible. While this simplifies code writing, unexpected type conversions can lead to errors. Languages like JavaScript exemplify this category.
2. Lifecycle
The lifecycle of a variable refers to the duration from when the variable is allocated in system memory to when the memory is released.
Declaration
The variable is introduced for the first time in the code. At this point, the variable is not yet allocated in actual memory.
Initialization
The initial value is assigned to the variable. At this stage, a specific location in memory is allocated to the variable, and the initial value is stored there.
Usage
After initialization, the variable can be used for read or write operations during program execution.
Release
When the variable is no longer needed, its memory is returned to the system for other uses. In languages with automatic memory management, this process is associated with garbage collection.
Destruction
Once the memory for the variable is released, the lifecycle of that variable ends. Any subsequent attempts to access that variable result in an error.
Depending on memory management, programming language, and the developer's choices, the lifecycle of a variable may vary slightly. It's crucial in languages like C++ and C to manage the lifecycle precisely, as neglecting to do so can lead to memory leaks or access errors.
Lifecycle in Go
- Variable Declaration in Go
In Go, there are multiple ways to declare a variable.
Declaration without Initialization:
var x int
For data types like int, int8, int16, etc.: the default value is 0.
For float32, float64: the default value is 0.0.
For bool: the default value is false.
For string: the default value is an empty string "".
For pointers, functions, interfaces, slices, channels, and maps: the default value is nil.
In this case, the default Zero Value is assigned based on the data type.
Variable Declaration with Initialization
var x int = 10
var a, b, c int = 1, 2, 3
Initialization using Type Inference
var x = 20
var x, y, z = 10, 20, 30
As the data type is not explicitly mentioned, the type is inferred based on the assigned value.
short declaration
z := 20
a, b, c := 10, 0.5, "hello"
In this case, type inference is also in effect.
Memory Allocation and Release in Go
In Go, when a variable is initialized, its memory allocation depends on the type of the variable and its declaration location.
Local Variables (Variables inside functions)
When a function is called, local variables are allocated in the stack memory. Once the function call completes, these variables are automatically released. This stack memory holds details of the called function's address, parameters, local variables, etc., forming a stack frame.
After the function call completes, the function's stack frame is removed from the stack. This entire process happens automatically, so there's no need for the programmer to manually release the memory.
Local variables inside control structures like if, while, for, etc., are also allocated on the stack following the same principles.
Here, the term "stack" doesn't refer to the conventional data structure but to a region of memory provided by the system based on the hardware architecture or platform.
Even if control structures become complex, compilers and runtimes can conveniently manage memory allocation and release using the stack pointer.
Package-level Variables (Global Variables)
Initialized global and static variables are allocated in the data section, whereas uninitialized global and static variables are allocated in the BSS (Block Started by Symbol) section.
In Go, they are initialized with zero values.
They are allocated when the program starts and are released from memory when the program terminates.
Different memory regions are used for variable allocation depending on their type and declaration location due to considerations of lifespan, scope, memory management efficiency, and memory usage optimization.
For instance, local variables that use the stack and follow the function's lifecycle are quickly allocated and released, and their LIFO (Last In First Out) nature makes their structure quite simple.
In contrast, global variables persist throughout the program's lifecycle. If used excessively, they can lead to unnecessary memory consumption.
Dynamic Allocation
In Go, keywords like new or make are used for dynamic memory allocation. This memory is allocated in the Heap (Note: the heap in data structures and the heap in memory management are different concepts).
The 'new' keyword is primarily used for basic data type memory allocation, and it returns the address (pointer) of the allocated memory. The allocated memory is initialized with zero values.
ptr := new(int)
*ptr = 10 // Initializes the memory area pointed by ptr to 10.
'make' is used in Go for allocating and initializing memory for special built-in types like slices, maps, and channels. The returned value isn't a pointer, but the initialized value.
s := make([]int, 5) // Creates a slice of length and capacity 5
s = append(s, 6) // The length becomes 6, and the internal array size can increase if needed.
For instance, make([]int, 5) creates and returns an int slice of length 5, with each element initialized to 0.
This slice possesses dynamic characteristics; it can expand or shrink based on its internal array.
This dynamic memory area can change its size during the program's runtime.
Developers need to manage allocation and release directly. If a developer doesn't manage the lifecycle properly, memory leaks can occur. In Go, garbage collection handles this. Memory that's no longer referenced gets released by the garbage collector. This is useful for storing dynamically sized data structures, like arrays, linked lists, trees, or objects that need to stay in memory for extended periods. While the heap area exists throughout the program's runtime, the lifespan of individual objects or data allocated in the heap lasts until they're released by the garbage collector's reference tracking. The benefit is that during runtime, based on memory requirements, memory can be allocated or released, enabling efficient memory use.
Also, data structures using dynamic memory allocation can easily handle operations like addition or deletion.
However, if there's no proper allocation and release by the programmer, issues like memory leaks and double releases can arise. Additionally, memory allocation and release require extra CPU time, and garbage collector operations can temporarily degrade performance.
If memory is repeatedly allocated and released, unused small memory chunks can occur, leading to memory fragmentation, which can hinder efficient memory usage.
접근제어자
변수편에서 누락된 부분이 있어 이어 정리한다.
Go에서는 간결성과 명확성을 추구하기에 별도의 키워드를 사용하지 않고 대소문자로 구분한다.
Exported
변수명이 대문자로 시작한다. 다른 패키지에서 접근 가능
Unexported
변수명이 소문자로 시작한다. 선언된 패키지내에서만 접근 가능
// mypackage.go
package mypackage
var privateVar = "Private Variable"
var PublicVar = "Public Variable"
// main.go
package main
import (
"fmt"
"mypackage"
)
func main() {
fmt.Println(mypackage.PublicVar) // 접근 가능
// fmt.Println(mypackage.privateVar) // 컴파일 에러: 접근 불가
}
패키지 수준의 변수
함수 밖에서 변수를 선언하면 패키지 내에서 전역적으로 사용할 수 있다.
package main
import "fmt"
// 패키지 수준에서의 변수 선언
var Name = "Monsangter"
func main() {
displayPackageName()
}
func displayPackageName() {
// 패키지 수준의 변수에 접근
fmt.Println(Name)
}
Go에서의 메모리 할당
go에서 변수 초기화시, 해당 변수의 메모리상 할당은, 그 변수의 종류와 선언 위치에 따라 다르다.
로컬 변수
함수의 호출이 시작될때 스택 메모리에 할당되며, 함수 호출이 종료되면 자동으로 해제된다. 이 스택 메모리에는 호출된 함수의 주소, 파라미터, 로컬 변수등을 저장하게 된다.
함수의 호출이 종료되면 해당 함수의 스택 프레임은 스택에서 제거되고, 이 모든 과정이 자동으로 이루어지기에 프로그래머가 별도의 메모리를 해제할 필요가 없다.